React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React Hooks 使用

React Hooks使用实战深度解析

作者:前端小D

useEffect是React提供的Hook,用于处理副作用操作(如数据获取、订阅、手动 DOM 修改等),这篇文章给大家介绍React Hooks使用实战,感兴趣的朋友跟随小编一起看看吧

目标:从原理到实践,掌握Hooks的正确使用和性能优化

1、useEffect 的执行时机具体是什么时候?它和 useLayoutEffect 有什么核心区别?

1、useEffect 的执行时机

useEffect 是 React 提供的 Hook,用于处理副作用操作(如数据获取订阅手动 DOM 修改等)。其执行时机遵循以下规则:

2、useLayoutEffect 的执行时机

useLayoutEffect 的 API 与 useEffect 相同,但执行时机不同

3、核心区别

4、代码示例

import { useEffect, useLayoutEffect } from 'react';
function Example() {
  useLayoutEffect(() => {
    // 同步执行,适合 DOM 测量或修改
    const element = document.getElementById('box');
    console.log('Layout effect:', element.offsetWidth);
  }, []);
  useEffect(() => {
    // 异步执行,适合数据获取等非紧急操作
    console.log('Effect triggered after paint');
  }, []);
  return <div id="box">Content</div>;
}

5、注意事项

2、你能深入剖析一下 useEffect 的依赖项吗?它背后的原理是什么?

1、useEffect 依赖项的深入剖析

依赖项数组用于确定 useEffect 是否应重新执行。当组件重新渲染时,React 会通过 Object.is 比较当前依赖项和上一次的依赖项值。若任一依赖项发生变化,副作用函数会重新执行;若依赖项为空数组 [],则副作用仅在组件挂载卸载时执行。

副作用函数会捕获定义时的闭包。若依赖项缺失函数内引用的变量可能为旧值。例如:

const [count, setCount] = useState(0);
useEffect(() => {
  console.log(count); // 若依赖项缺失,可能打印过时的 count
}, []); // 应改为 [count]

2、依赖项优化的原理

React每次渲染时会生成新的副作用函数依赖项的比较发生在渲染提交阶段。若依赖项是引用类型(如对象数组),即使内容相同,引用变化也会触发重新执行。此时可通过 useMemo 或 useCallback 稳定引用。

ESLint 插件 exhaustive-deps 会静态分析代码中的依赖关系,但无法识别动态依赖(如循环生成的依赖项)。手动维护依赖项时需确保逻辑一致性。

3、依赖项与性能的权衡

添加过多依赖项可能导致频繁执行副作用。可通过拆分多个 useEffect 或提取稳定值来优化:

useEffect(() => {
  const timer = setInterval(() => {}, delay);
  return () => clearInterval(timer);
}, [delay]); // 仅当 delay 变化时重建定时器

每次副作用重新执行前,会先执行上一次的清除函数(若存在)。依赖项变化频率直接影响资源清理和重建的开销。

4、高级模式与原理

通过在副作用函数内部添加条件判断可减少实际操作的触发次数但依赖项仍需完整声明

useEffect(() => {
  if (isValid(data)) {
    fetchData(data);
  }
}, [data]); // 即使有内部判断,data 仍需作为依赖

对于事件处理等场景,使用 useCallback 可避免因函数引用变化导致的依赖项失效

const fetchData = useCallback(() => {
  // 逻辑代码
}, [query]); // query 变化时才更新函数引用

通过理解依赖项比对机制闭包特性性能影响,可以更精准地控制副作用的行为。实践时应结合具体场景平衡依赖项的完整性与执行效率

3、在 useEffect 中,如何正确地处理异步请求并避免竞态条件(Race Condition)?

1、在 useEffect 中处理异步请求

使用 useEffect 处理异步请求时,需要确保组件卸载时取消未完成的请求。可以通过 AbortController 实现请求的取消功能

useEffect(() => {
  const abortController = new AbortController();
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', {
        signal: abortController.signal
      });
      const data = await response.json();
      // 处理数据
    } catch (error) {
      if (error.name !== 'AbortError') {
        // 处理非取消错误
      }
    }
  };
  fetchData();
  return () => {
    abortController.abort();
  };
}, []);

2、避免竞态条件

竞态条件发生在多个请求按不同顺序返回时,导致最终显示的数据与预期不符。可以通过以下方式避免:

useEffect(() => {
  let ignore = false;
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    if (!ignore) {
      // 更新状态
    }
  };
  fetchData();
  return () => {
    ignore = true;
  };
}, [dependency]);
useEffect(() => {
  let requestId = 0;
  const fetchData = async () => {
    const currentRequestId = ++requestId;
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    if (currentRequestId === requestId) {
      // 更新状态
    }
  };
  fetchData();
  return () => {
    requestId = -1;
  };
}, [dependency]);
useEffect(() => {
  const currentRequest = { active: true };
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    if (currentRequest.active) {
      // 更新状态
    }
  };
  fetchData();
  return () => {
    currentRequest.active = false;
  };
}, [dependency]);

3、关键点总结

4、什么时候应该使用 useCallback 和 useMemo?如果滥用它们会带来什么后果?

1、useCallback 的使用场景

useCallback 主要用于优化性能避免不必要的函数重新创建。当函数作为依赖项传递给子组件(尤其是通过 React.memo 优化的子组件)时,使用 useCallback 可以防止因父组件重新渲染导致子组件不必要的更新。典型场景包括:

2、useMemo 的使用场景

useMemo 用于缓存昂贵的计算结果避免在每次渲染时重复计算。适用于以下情况:

3、滥用 useCallback 和 useMemo 的后果

过度使用这些 Hook 可能导致以下问题:

4、实际应用建议

5、useRef 有哪些常见的应用场景?它和 useState 的根本区别是什么?

1、useRef 的常见应用场景

useRef 创建的 ref 对象可以在组件的整个生命周期中持久化存储可变值,修改其 .current 属性不会导致组件重新渲染。适用于保存计时器 IDDOM 节点引用任何需要在渲染间保持稳定的数据

通过将 ref 绑定到 JSX 的 ref 属性,可以获取或操作真实的 DOM 节点。例如自动聚焦输入框测量元素尺寸集成第三方库(如 D3.js)时直接操作 DOM

当需要缓存函数组件内的高开销计算结果,且不希望因状态更新重复计算时,可用 useRef 保存计算结果,仅在依赖项变化时重新计算。

通过结合 useEffect,可以用 useRef 存储上一次的状态或 props 值,用于比较当前和之前的差异

2、useRef 与 useState 的根本区别

修改 useState 的状态值会触发组件重新渲染,而修改 useRef 的 .current 属性不会引起重新渲染。这是两者最核心的行为差异。

useState 的状态更新是异步的React批量处理;而 useRef 的 .current 修改是同步的,立即生效。例如在事件处理中直接访问 .current 会得到最新值。

useState 用于管理需要触发 UI 更新的状态数据useRef 更偏向于存储与渲染无关的可变值直接操作 DOM 的副作用场景

示例代码对比

// useState 示例:点击按钮会触发渲染
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1); // 触发重新渲染
// useRef 示例:点击按钮不会触发渲染
const countRef = useRef(0);
const handleClick = () => {
  countRef.current += 1; // 不触发渲染
  console.log(countRef.current);
};

6、forwardRef 和 useImperativeHandle 这两个 Hooks 是为了解决什么特定问题而设计的?

1、forwardRef 和 useImperativeHandle 的设计目的

forwardRef 和 useImperativeHandle 是 React 提供的两个高级 Hooks,主要用于解决父组件需要直接访问子组件内部 DOM 节点或自定义方法的场景。以下是它们的具体作用:

2、forwardRef 的作用

forwardRef 允许父组件通过 ref 直接获取子组件DOM节点 或 自定义实例。在 React 中,默认情况下 ref 无法直接透传到函数式子组件,因为函数式组件没有实例forwardRef 通过将 ref 作为第二个参数传递,解决了这一问题。

const ChildComponent = forwardRef((props, ref) => {
  return <div ref={ref}>子组件内容</div>;
});
function ParentComponent() {
  const childRef = useRef();
  return <ChildComponent ref={childRef} />;
}

3、useImperativeHandle 的作用

useImperativeHandle 允许子组件自定义通过 ref 暴露给父组件的属性或方法。默认情况下,ref 只能获取 DOM 节点,但通过 useImperativeHandle,可以暴露子组件的特定功能,避免直接暴露全部内部实现。

const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));
  return <input ref={inputRef} />;
});
function ParentComponent() {
  const childRef = useRef();
  const handleClick = () => childRef.current.focus();
  return (
    <>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>聚焦输入框</button>
    </>
  );
}

4、解决的问题总结

7、React 18 新增的 useId 解决了什么问题( SSR 场景下的 ID 冲突、可访问性问题)?

1、useId 解决的问题

React 18 引入的 useId Hook 主要用于解决以下两类问题:

服务器端渲染(SSR)时,组件生成的 ID 可能在客户端与服务端不一致导致 hydration 错误。传统手动生成 ID 的方式(如 Math.random()无法保证跨环境的一致性,而 useId 通过 React 的内部机制确保生成的 ID 在服务端和客户端保持一致。

需要唯一 ID 的场景(如表单元素的 htmlFor 与 id 关联、ARIA 属性)若未正确处理,可能导致可访问性工具无法正确解析。useId 生成的稳定 ID 能避免此类问题,同时支持跨组件调用时的唯一性。

2、useId 的核心特性

生成的 IDSSR 和客户端渲染中完全一致,避免 hydration 不匹配错误。

同一组件多次调用 useId 会生成不同的后缀(如 :r1::r2:),确保同一组件内的多个 ID 不冲突。

ID 格式为 :r{前缀}:{计数器},前缀由 React 内部管理,避免与其他库冲突。

3、使用示例

import { useId } from 'react';
function Checkbox() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Accept terms</label>
      <input id={id} type="checkbox" />
    </>
  );
}

4、与传统方案的对比

方案服务端/客户端一致性可访问性支持代码简洁性
useId
Math.random⚠️
UUID 库⚠️

5、注意事项

8、useTransition 和 useDeferredValue 是如何优化用户体验的?它们之间有什么区别?

1、useTransition 和 useDeferredValue 的作用

useTransition 和 useDeferredValue 是 React 18 引入的并发特性(Concurrent Features)旨在优化用户界面响应速度,减少渲染阻塞带来的卡顿问题。它们通过将非紧急更新标记为“可中断”,让高优先级交互(如用户输入)能够优先处理。

2、useTransition 的优化机制

useTransition 允许将状态更新标记为“过渡”(非紧急),并返回一个isPending标志用于界面反馈。适用于需要延迟渲染但需明确控制过渡状态的场景

const [isPending, startTransition] = useTransition();
startTransition(() => {
  // 非紧急状态更新(如搜索结果筛选)
  setFilter(inputValue);
});

优化效果

3、useDeferredValue 的优化机制

useDeferredValue 接收一个值并返回其延迟版本,React 会在后台处理更新。适用于派生状态或计算密集型渲染的场景。

const deferredValue = useDeferredValue(value);
// 基于 deferredValue 渲染复杂组件
return <ExpensiveComponent value={deferredValue} />;

优化效果:

4、核心区别

特性useTransitionuseDeferredValue
适用场景明确的状态更新(如按钮点击)派生值或计算密集型渲染
控制粒度需手动调用startTransition自动延迟值的更新
反馈机制提供isPending标志无直接反馈,依赖新旧值对比
底层实现标记更新优先级基于useTransition的封装

5、如何选择?

9、你能详细解释一下 React Hooks 的执行顺序和依赖规则吗?

1、React Hooks 的执行顺序规则

2、useEffect 的依赖规则

3、useMemo 和 useCallback 的依赖规则

useMemo 用于记忆计算结果,只有当依赖项变化时才会重新计算。useCallback 用于记忆函数引用依赖项变化时会返回新函数

过度使用这两个 Hook 可能导致性能问题而非优化。它们适用于计算开销大或需要稳定引用的情况

4、自定义 Hook 的依赖传递

自定义 Hook 内部使用的 Hooks 依赖项应该暴露给外部组件。这可以通过参数传递实现,确保依赖关系清晰可见。自定义 Hook依赖变化会触发所有使用该 Hook 的组件更新

5、Hook 闭包问题

Hooks 经常遇到闭包捕获旧值的问题,特别是在异步操作中。可以通过 useRef 保持可变引用,或确保依赖数组包含所有变化值来解决。每次渲染都有独立的 propsstateeffects,这是故意设计的行为。

6、多个 Effect 的执行顺序

同一组件中的多个 useEffect按照声明顺序依次执行清理函数在组件卸载或依赖变化前执行且执行顺序与 Effect 相反LayoutEffect 的触发时机比 useEffect 早,会在 DOM 更新后同步执行

到此这篇关于React Hooks使用实战深度解析的文章就介绍到这了,更多相关React Hooks 使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文