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. 如果每次都传新对象、新数组、新函数,它的收益可能很有限

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


5. useMemouseCallbackReact.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. useLayoutEffectuseEffect 的区别

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

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. useTransitionuseDeferredValue 是什么

这两个 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,通常更偏样式库、底层库作者场景,而不是日常业务组件开发主线。

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


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 把 useMemouseCallback 当性能银弹

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

14.2 以为 useReducer 一定比 useState 高级

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

14.3 在普通副作用里滥用 useLayoutEffect

这会阻塞浏览器绘制。

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

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


15. 一句话总结

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

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