Skip to content

Hooks 进阶:memo、callback、reducer 与并发能力 ​

React 学到后面,真正容易讲乱的往往不是 useState,而是这些“看起来像优化或进阶”的能力:

  1. useMemo
  2. useCallback
  3. React.memo
  4. useReducer
  5. useLayoutEffect
  6. useImperativeHandle
  7. useId
  8. useTransition
  9. useDeferredValue
  10. useSyncExternalStore

这篇文章重点讲:

  1. 这些 Hook 和 API 分别在解决什么问题
  2. 它们什么时候值得用
  3. 它们最容易被误用的地方是什么

1. 为什么这些 Hook 容易被滥用 ​

因为它们看起来都像“高级优化按钮”。

但 React 里很多进阶 Hook 真正的定位其实是:

  1. 有些用于优化引用稳定性
  2. 有些用于组织复杂状态更新
  3. 有些用于处理特殊副作用时机
  4. 有些用于并发和交互优先级

🌟 一个很重要的工程判断是,不是越多 Hook 越高级,而是越知道边界越高级。


2. useMemo 到底在缓存什么 ​

useMemo 缓存的是计算结果。

例如:

jsx
function ProductTable({ products, keyword }) {
  const filteredProducts = useMemo(() => {
    return products.filter(product => product.name.includes(keyword));
  }, [products, keyword]);

  return <Table rows={filteredProducts} />;
}

它更适合:

  1. 计算明显有成本
  2. 结果需要在依赖不变时复用
  3. 结果引用稳定本身也有价值

它不适合:

  1. 非常轻的计算
  2. 只是“看起来专业所以先加上”

3. useCallback 到底在缓存什么 ​

useCallback 缓存的是函数引用。

jsx
function UserPage({ userId }) {
  const handleRefresh = useCallback(() => {
    console.log("refresh", userId);
  }, [userId]);

  return <Toolbar onRefresh={handleRefresh} />;
}

它更适合:

  1. 回调会传给被 React.memo 包裹的子组件
  2. 你确实需要一个稳定函数引用
  3. 某些依赖关系必须保持稳定

如果子组件本来也不会因为函数引用变化造成实际问题,那就没必要为了“优化”而强上。


4. React.memo 在解决什么问题 ​

React.memo 作用在组件层。

它的核心目标是,当 props 没变时,跳过不必要的重复渲染。

jsx
const UserCard = React.memo(function UserCard({ user, onSelect }) {
  return (
    <div onClick={() => onSelect(user.id)}>
      {user.name}
    </div>
  );
});

但要注意:

  1. React.memo 只对 props 比较有效
  2. 默认比较是浅比较
  3. 如果每次都传新对象、新数组、新函数,它的收益可能很有限

所以很多时候它要和 useMemo、useCallback 一起看,而不是单独看。


5. useMemo、useCallback、React.memo 怎么串起来理解 ​

可以这样分工:

能力缓存什么
useMemo计算结果
useCallback函数引用
React.memo组件渲染结果是否可跳过

它们常见的配合方式是:

  1. 父组件用 useMemo 保持对象结果稳定
  2. 父组件用 useCallback 保持函数引用稳定
  3. 子组件用 React.memo 减少重复渲染

但一定要先确认问题真的存在,再去做这层优化。


6. useReducer 适合什么场景 ​

useReducer 适合状态更新逻辑比较复杂的场景。

例如:

  1. 同一块状态有很多种更新动作
  2. 更新规则需要集中管理
  3. 你希望把“状态”和“如何更新状态”拆开
jsx
function formReducer(state, action) {
  switch (action.type) {
    case "change_name":
      return { ...state, name: action.payload };
    case "change_email":
      return { ...state, email: action.payload };
    case "reset":
      return { name: "", email: "" };
    default:
      return state;
  }
}

function UserForm() {
  const [state, dispatch] = useReducer(formReducer, {
    name: "",
    email: ""
  });

  return (
    <>
      <input
        value={state.name}
        onChange={e => dispatch({ type: "change_name", payload: e.target.value })}
      />
      <input
        value={state.email}
        onChange={e => dispatch({ type: "change_email", payload: e.target.value })}
      />
      <button onClick={() => dispatch({ type: "reset" })}>重置</button>
    </>
  );
}

它不一定比 useState 更高级,但在复杂状态流里更容易把更新逻辑收口。


7. useLayoutEffect 和 useEffect 的区别 ​

两者都处理副作用,但时机不同。

Hook执行时机更适合什么场景
useEffect浏览器绘制后请求、订阅、一般副作用
useLayoutEffectDOM 变更后、绘制前读布局、同步改样式、避免闪动

例如:

jsx
function Tooltip() {
  const ref = useRef(null);

  useLayoutEffect(() => {
    const rect = ref.current?.getBoundingClientRect();
    console.log(rect);
  }, []);

  return <div ref={ref}>tooltip</div>;
}

🌟 useLayoutEffect 不是更高级的 useEffect,而是更强约束的同步时机工具,不能滥用。


8. useImperativeHandle 在解决什么问题 ​

React 默认更强调声明式数据流。

但某些场景下,父组件确实希望直接触发子组件暴露的方法,例如:

  1. 手动聚焦
  2. 打开弹层
  3. 重置内部表单

这时可以通过 forwardRef + useImperativeHandle 暴露一个受控的命令式接口。

jsx
const FormDialog = forwardRef(function FormDialog(_, ref) {
  const [open, setOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    open: () => setOpen(true),
    close: () => setOpen(false)
  }), []);

  return open ? <div>dialog</div> : null;
});

它的价值不是鼓励命令式编程,而是 在少量必须命令式控制的场景里,把暴露面收窄。


9. useId 适合什么场景 ​

useId 主要用来生成稳定的、适合无障碍关联的 id。

例如:

jsx
function SearchField() {
  const inputId = useId();

  return (
    <>
      <label htmlFor={inputId}>关键词</label>
      <input id={inputId} />
    </>
  );
}

它比手写随机 id 更适合 React 渲染模型,尤其是在服务端渲染和水合场景下更稳。


10. useTransition 和 useDeferredValue 是什么 ​

这两个 Hook 都和“交互优先级”有关。

10.1 useTransition ​

它更适合把某些“不那么紧急”的状态更新标记成低优先级。

例如搜索框里:

  1. 输入框文本更新要立刻响应
  2. 大列表筛选可以稍微晚一点更新
jsx
function SearchPage({ products }) {
  const [keyword, setKeyword] = useState("");
  const [isPending, startTransition] = useTransition();

  const [filtered, setFiltered] = useState(products);

  const handleChange = event => {
    const value = event.target.value;
    setKeyword(value);

    startTransition(() => {
      setFiltered(products.filter(item => item.name.includes(value)));
    });
  };

  return (
    <>
      <input value={keyword} onChange={handleChange} />
      {isPending ? <div>筛选中...</div> : <List items={filtered} />}
    </>
  );
}

10.2 useDeferredValue ​

它更适合把一个值的“低优先级版本”延后一点使用。

例如输入框文本立即变化,但大列表筛选使用延后的关键字。


11. useSyncExternalStore 在解决什么问题 ​

这个 Hook 更偏和“外部状态源”对接。

例如:

  1. 自己实现的小型状态库
  2. 浏览器 API 状态
  3. 不由 React 自己直接托管的外部 store

它解决的是,让 React 以更稳定的方式订阅外部状态,并保持并发场景下一致性。

普通业务开发里不会天天写,但做状态库或接第三方 store 时很关键。


12. useDebugValue 和其他少用 Hook ​

12.1 useDebugValue ​

更适合在自定义 Hook 里给开发工具展示更友好的调试标签。

12.2 其他更少用的 Hook ​

像 useInsertionEffect 这类 Hook,通常更偏样式库、底层库作者场景,而不是日常业务组件开发主线。

所以学习顺序上不应该把它们放得和 useState、useEffect 一样重。


13. 自定义 Hook 的价值是什么 ​

自定义 Hook 不是“另一个组件”。

它真正重要的是,把一段可复用的状态逻辑和副作用逻辑提取出来。

例如:

jsx
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return width;
}

这个 Hook 的重点不是“减少几行代码”,而是让“窗口宽度订阅”这段逻辑可以稳定复用。


14. 进阶 Hooks 最常见的误区 ​

14.1 把 useMemo、useCallback 当性能银弹 ​

它们有成本,不能默认全加。

14.2 以为 useReducer 一定比 useState 高级 ​

它只是更适合复杂更新规则。

14.3 在普通副作用里滥用 useLayoutEffect ​

这会阻塞浏览器绘制。

14.4 不理解并发相关 Hook 的目标 ​

useTransition、useDeferredValue 的重点是交互优先级,不是“神奇提速按钮”。


15. 一句话总结 ​

React 进阶 Hooks 的主线,本质上是在解决 如何稳定复用状态逻辑、如何减少不必要的重复工作、如何在复杂场景下组织更新优先级,以及如何把少量特殊场景的命令式能力控制在边界之内。

基于 VitePress 构建的个人技术笔记。