React常用Hooks全面解析:16个核心钩子的概念与使用场景

摘要:本文整理了React开发中最常用的16个Hook,分为6大类,覆盖日常开发的大部分场景。内容基于实际使用经验手打整理,未包含React 19新增特性。

本文整理了React开发中最常用的16个Hook,分为6大类,覆盖日常开发的大部分场景。内容基于实际使用经验手打整理,未包含React 19新增特性。


一、六大分类概览

分类Hook名称用途
State Management(状态管理)useState定义组件状态
State Management(状态管理)useReducer复杂状态管理
State Management(状态管理)useSyncExternalStore订阅外部数据源
Effect Hooks(副作用)useEffect处理副作用
Effect Hooks(副作用)useLayoutEffect同步执行副作用
Effect Hooks(副作用)useInsertionEffect注入样式专用
Ref Hooks(引用)useRef创建可变引用
Ref Hooks(引用)useImperativeHandle暴露子组件方法
Performance Hooks(性能优化)useMemo缓存计算结果
Performance Hooks(性能优化)useCallback缓存函数引用
Context Hooks(上下文)useContext读取上下文值
Transition Hooks(过渡)useTransition标记非紧急更新
Transition Hooks(过渡)useDeferredValue延迟更新状态值
Random Hooks(杂项)useDebugValue自定义Hook调试
Random Hooks(杂项)useId生成唯一ID

下面按分类逐一说明每个Hook的用法和适用场景。


二、State Management(状态管理)

状态管理是React最核心的功能——当状态发生变化时,自动重新渲染组件。这个分类包含三个Hook。

useState - 定义组件状态

使用频率最高的Hook,用于定义组件的局部状态。

const [value, setValue] = useState('');  // 设置状态初始值

const handleChange = (e) => {  
  setValue(e.target.value);  // 事件触发时更新状态
};

<input
  type="text"
  value={value}
  onChange={handleChange}
/>

第一个变量value存储当前状态值,第二个变量setValue是更新状态的函数。上面的例子中,输入框内容会实时同步到value状态中。

useReducer - 复杂状态管理

比useState更强大,适合管理复杂的状态逻辑。虽然使用频率较低,但在特定场景下优势明显。

const reducer = (state, action) => {
  switch (action) {
    case 'increment':
      return state + 1;
    default:
      return state;
  }
};

const [count, dispatch] = useReducer(reducer, 0);

<button onClick={() => dispatch('increment')}>Increment</button>

dispatch函数接收一个action参数,可以根据不同的action做出灵活的状态变更。在游戏界面这类交互复杂、状态繁多的场景中,useReducer比useState更适用。

useSyncExternalStore - 订阅外部数据源

这个Hook比较冷门,用于将非React的状态(如第三方库、浏览器API)接入到React组件中,实现外部数据源的订阅。


三、Effect Hooks(副作用)

副作用指的是组件渲染过程中与渲染结果无直接关联、但会影响外部环境的一些操作。

useEffect - 处理副作用

const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `你点击了 ${count} 次`;
}, [count]);

<button onClick={() => setCount(count + 1)}>
  Click me
</button>

第二个参数传入[count],表示只有当count变化时,才会执行回调函数。需要注意,useEffect不应该用于“点击按钮时执行”或“获取数据时执行”这类场景。

实际上useEffect并不需要频繁使用。它最合适的场景是让React与浏览器API同步,比如控制视频(或音频)的播放与暂停:

const ref = useRef(null); // 创建引用,绑定视频DOM元素

useEffect(() => {
  if (isPlaying) {
    ref.current.play();  // 播放
  } else {
    ref.current.pause();  // 暂停
  }
}, [isPlaying]);

<video ref={ref} src={src} loop playsInline />

当isPlaying设置为true时,视频开始播放。这种与外部系统同步的场景,才是useEffect真正发挥作用的地方。

useLayoutEffect - 同步执行副作用

useLayoutEffect在浏览器完成渲染之前同步执行,可以用来避免页面闪烁或布局跳动。

const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);

useLayoutEffect(() => {
  const { height } = ref.current.getBoundingClientRect();  // 获取提示框高度
  setTooltipHeight(height);  // 设置高度
}, []);

浏览器还没开始渲染这个提示框,我们就已经获取到它的高度并保存为状态。这样在后续渲染中可以提前布局,避免视觉上的跳跃。

useInsertionEffect - 注入样式专用

这是一个更加小众的Hook,执行时机比useLayoutEffect还早,专门为CSS-in-JS库开发者设计,用于确保所有CSS样式都能正确应用到对应的组件元素上。


四、Ref Hooks(引用)

useRef - 创建可变引用

useRef类似于useState,也能存储值,但不会触发组件重新渲染。引用是可变的,可以直接通过等号运算符修改,例如ref.current = 'something'。

const [timer, setTimer] = useState(0);
const intervalRef = useRef(); // useRef用来保存setInterval的ID

const startTimer = () => {
  intervalRef.current = setInterval(() => {  // 存储定时器ID
    setTimer((prevTimer) => prevTimer + 1);
  }, 1000);
};

const stopTimer = () => {
  clearInterval(intervalRef.current);  // 停止时清除定时器
};

<p>Timer: {timer} seconds</p>
<button onClick={startTimer}>开始计时</button>
<button onClick={stopTimer}>停止计时</button>

你可能会问,为什么不用普通变量?因为React组件每次重新渲染,普通变量的值会丢失。使用useRef保存的值在组件整个生命周期中都存在。

useImperativeHandle - 暴露子组件方法

这个Hook配合forwardRef使用,用于向父组件暴露子组件的方法。

function ParentComponent() {
  const inputRef = useRef();

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <CustomInput ref={inputRef} />
      <button onClick={handleFocus}>激活input</button>
    </>
  );
}

// 使用forwardRef + useImperativeHandle暴露子组件方法
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
  }));

  return <input ref={inputRef} />;
});

因为直接给自定义组件(如CustomInput)传递ref,默认情况下是拿不到内部DOM元素的。useImperativeHandle解决了这个问题,让父组件可以调用子组件暴露的方法。


五、Performance Hooks(性能优化)

useMemo - 缓存计算结果

useMemo通过记忆化技术缓存计算结果,避免不必要的重复计算。只有在依赖项发生变化时才会重新计算。

function SumComponent({ numbers }) {
  const sum = useMemo(() => {  // 缓存求和结果
    return numbers.reduce((total, n) => total + n, 0);
  }, [numbers]);

  return <h1>合计: {sum}</h1>;
}

求和结果被缓存起来,组件重新渲染时不会重复执行reduce计算。只有当numbers数组发生变化时,缓存才会更新。

useCallback - 缓存函数引用

useCallback与useMemo类似,但专门用于缓存函数。

function Counter() {
  const [count, setCount] = useState(0);

  // 缓存increment函数
  const increment = useCallback(() => {
    setCount((c) => c + 1);
  }, []);

  return (
    <>
      <div>{count}</div>
      <Button onClick={increment} />
    </>
  );
}

// Button组件
function Button({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

每次组件重新渲染时,内部定义的函数都会被重新创建。使用useCallback后,只要依赖项不变,函数引用就不会改变,避免子组件不必要的重新渲染。


六、Context Hooks(上下文)

useContext - 读取上下文值

useContext用于读取上下文的值,可以理解为React版的全局变量。

const ThemeContext = createContext();

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  );
}

function Form() {
  const theme = useContext(ThemeContext);
  // ...
}

createContext创建一个上下文,通过Provider提供给子组件,其他组件中通过useContext即可读取到该值。这个模式常用于主题切换、语言偏好等全局配置。


七、Transition Hooks(过渡)

useTransition - 标记非紧急更新

useTransition可以将某些状态更新标记为非紧急,避免阻塞用户交互。

const [filter, setFilter] = useState('');
const [inputValue, setInputValue] = useState('');
const [isPending, startTransition] = useTransition();

const filteredItems = items.filter(item => item.includes(filter));

<input
  value={inputValue}
  onChange={(event) => {
    setInputValue(event.target.value);
    startTransition(() => {
      setFilter(event.target.value);
    });
  }}
  placeholder="请填充内容...."
/>

{isPending ? (
  <p>加载中...</p>
) : (
  filteredItems.map(item => <div key={item}>{item}</div>)
)}

在输入框中输入时,如果每次输入都重新渲染整个列表,会导致UI卡顿。用startTransition包裹过滤逻辑后,这段更新被标记为低优先级。isPending是一个布尔值,表示当前是否有状态更新正在等待中,可以在更新完成前显示加载提示。

useDeferredValue - 延迟更新状态值

useDeferredValue与useTransition类似,但它会在合适的时机自动延迟更新状态值。

const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);

一个完整的过滤列表示例:

function FilteredList({ items }) {
  const [inputValue, setInputValue] = useState('');
  const deferredFilter = useDeferredValue(inputValue);

  const filteredItems = items.filter(item =>
    item.toLowerCase().includes(deferredFilter.toLowerCase())
  );

  return (
    <>
      <input
        value={inputValue}
        onChange={(event) => setInputValue(event.target.value)}
        placeholder="Type to filter..."
      />
      {filteredItems.map(item => <div key={item}>{item}</div>)}
    </>
  );
}

useDeferredValue很适合过滤列表这类场景,无需手动设置过渡,也不需要任何异步状态管理。


八、Random Hooks(杂项)

useDebugValue - 自定义Hook调试

在React DevTools中为自定义Hook添加标签,方便调试时查看内部状态。

useId - 生成唯一ID

调用useId会生成一个唯一的ID,通常用于表单输入框之间的ID共享。

function EmailInput({ name }) {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>{name}</label>
      <input id={id} type="email" />
    </>
  );
}

function Form() {
  return (
    <form>
      <EmailInput name="Email" />
      <EmailInput name="Confirm Email" />
    </form>
  );
}

在Form中两次使用EmailInput组件,通过useId生成的唯一ID可以避免输入框冲突,无需手动管理ID。


总结

以上16个Hook覆盖了React日常开发的绝大多数场景。回顾一下核心要点:

  • 状态管理三件套:useState处理简单状态,useReducer应对复杂逻辑,useSyncExternalStore处理外部数据源。

  • 副作用处理有执行时机之分:useEffect在渲染后执行,useLayoutEffect在渲染前执行,useInsertionEffect专门处理样式注入。

  • 引用不同于状态:useRef的值可变且不触发重渲染,适合存储DOM引用或不需要响应式更新的值。

  • 性能优化靠缓存:useMemo缓存计算结果,useCallback缓存函数引用,减少不必要的重新计算和渲染。

  • 过渡机制提升体验:useTransition和useDeferredValue将非紧急更新延后处理,保证交互流畅。

理解这些Hook的执行时机和适用场景,才能在合适的场景选择合适的工具。不是所有场景都需要用Hook,也不是所有优化都值得加。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13717