Skip to main content

其他 Hooks

useMemo vs useCallback

二者都是为了缓存,避免每次渲染重新计算/重新创建引用。

  • useMemo(fn, deps):缓存 fn 的返回值(计算结果)。
  • useCallback(fn, deps):缓存 函数本身useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
// 缓存昂贵计算的结果
const sorted = useMemo(() => list.sort(compare), [list]);

// 缓存函数引用,配合 React.memo 子组件,避免子组件无谓重渲染
const handleClick = useCallback(() => doSomething(id), [id]);

面试题:是不是所有函数都该用 useCallback 包一层? 不是。useCallback/useMemo 本身有缓存成本(保存函数、比较依赖)。只有在「① 传给被 React.memo 包裹的子组件」或「② 作为其他 Hook 的依赖项」时才有意义,否则属于过度优化。

React.memo

React.memo(Component) 对 props 做浅比较,props 没变就跳过子组件渲染。

注意三个常见失效点:① 传了每次都新建的对象/函数/数组(需配合 useMemo/useCallback);② children 是 JSX 时引用每次都变;③ 用了会变的 Context。

useRef

两个用途:

  1. 获取 DOM 节点const ref = useRef(null); <input ref={ref} />
  2. 存储「跨渲染保持、但变化不触发重渲染」的可变值:如定时器 id、上一次的值、最新的 state(解决闭包陷阱)。
// 用 useRef 保存最新值,解决 setInterval 闭包陷阱
const countRef = useRef(count);
countRef.current = count; // 每次渲染同步最新值
useEffect(() => {
const timer = setInterval(() => console.log(countRef.current), 1000);
return () => clearInterval(timer);
}, []);

useRef vs useState:改 ref.current 不会触发重渲染、是同步的;setState 会触发重渲染、是「异步」批处理的。

useRef vs 普通变量:函数组件每次渲染普通变量都会重新声明,而 ref.current 在整个组件生命周期内保持同一引用。

useContext

订阅最近的 Provider 的值,Provider 的 value 变化会让所有消费组件重渲染(性能陷阱与优化见 组件通信)。

useReducer

适合复杂状态逻辑(多个子值、下一个 state 依赖前一个、状态转移有规律)。是 useState 的进阶。

function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "reset":
return { count: 0 };
default:
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "increment" });

useEffect vs useLayoutEffect

这两个 Hook 的使用方式完全一样,唯一的区别在于执行时机

  • useEffect (绝大多数情况首选):在浏览器完成 DOM 绘制 (Paint) 之后,异步延迟执行。不会阻塞主线程。
  • useLayoutEffect:在 React 更新了 DOM 之后、但浏览器把这些变更真正画到屏幕上 (Paint) 之前,同步阻塞执行。

面试题:什么时候必须用 useLayoutEffect 如果你的 effect 中包含读取 DOM 布局(如 offsetWidthscrollTop)并且会同步修改 State 从而引发重新渲染的代码,必须用 useLayoutEffect。 如果用 useEffect,浏览器会先把错误的旧布局画到屏幕上,然后你的 effect 修改了 state,浏览器又瞬间画出新的布局,用户会看到明显的UI 闪烁。 注意:useLayoutEffect 会阻塞浏览器渲染,慎用,否则会导致卡顿。

面试扩展:useLayoutEffect 就是用来平替 getSnapshotBeforeUpdate 的吗? 并不是! 这是一个极易混淆的陷阱题。

  • getSnapshotBeforeUpdate 的触发时机是:React 算好了虚拟 DOM,但还没有将真实 DOM 更新到页面上。它读取的是“旧 DOM”的最后遗像。 (业务场景:比如你在做一个微信聊天室,别人发来新消息,列表会被撑长。为了不让用户的阅读视线被打断,你必须在 DOM 插入新消息前的一瞬间,量出当前的 scrollHeight,然后把这个值作为 snapshot 传给 componentDidUpdate,等新消息渲染完,立刻把滚动条推回到之前的位置。这就是读取旧 DOM 的核心价值)
  • useLayoutEffect 的触发时机是:React 已经将真实 DOM 更新完毕了,只是浏览器还没来得及 Paint 绘制出像素而已。它读取的是“新 DOM”的尺寸。
  • 在函数组件中,目前依然没有能够平替 getSnapshotBeforeUpdate 的 Hook。

useImperativeHandle + forwardRef

让父组件能通过 ref 调用子组件暴露的方法(命令式),常用于封装组件库。

const Input = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
}));
return <input ref={inputRef} />;
});

React 19 起函数组件可直接把 ref 作为普通 prop 接收,不再强制 forwardRef

useDebugValue (了解即可)

这个 Hook 是专门给自定义 Hook 的开发者用的,业务组件里几乎用不到。 它的作用是:在 React DevTools(浏览器调试插件)中,为你封装的自定义 Hook 旁边显示一个自定义的调试标签。

function useOnlineStatus() {
const isOnline = useSyncExternalStore(...);
// 在 React DevTools 里,这个 Hook 旁边会显示 "Online: true"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}

useSyncExternalStore

React 18 提供,用于订阅外部数据源(如 Redux/Zustand 等状态库、localStorage、媒体查询),解决并发渲染下的「撕裂(tearing)」问题。

const width = useSyncExternalStore(
(cb) => {
window.addEventListener("resize", cb);
return () => window.removeEventListener("resize", cb);
},
() => window.innerWidth, // 获取快照
() => 0, // SSR 时的快照
);

小结

对比要点
useMemo / useCallback缓存值 / 缓存函数;按需使用
useEffect / useLayoutEffect异步、不阻塞绘制 / 同步、DOM 变更后绘制前执行
useState / useReducer简单状态 / 复杂状态转移
useRef / useState不触发渲染、同步 / 触发渲染、批处理