其他 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
两个用途:
- 获取 DOM 节点:
const ref = useRef(null); <input ref={ref} />。 - 存储「跨渲染保持、但变化不触发重渲染」的可变值:如定时器 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 布局(如offsetWidth、scrollTop)并且会同步修改 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 | 不触发渲染、同步 / 触发渲染、批处理 |