Appearance
Hooks 进阶:memo、callback、reducer 与并发能力
React 学到后面,真正容易讲乱的往往不是 useState,而是这些“看起来像优化或进阶”的能力:
useMemouseCallbackReact.memouseReduceruseLayoutEffectuseImperativeHandleuseIduseTransitionuseDeferredValueuseSyncExternalStore
这篇文章重点讲:
- 这些 Hook 和 API 分别在解决什么问题
- 它们什么时候值得用
- 它们最容易被误用的地方是什么
1. 为什么这些 Hook 容易被滥用
因为它们看起来都像“高级优化按钮”。
但 React 里很多进阶 Hook 真正的定位其实是:
- 有些用于优化引用稳定性
- 有些用于组织复杂状态更新
- 有些用于处理特殊副作用时机
- 有些用于并发和交互优先级
🌟 一个很重要的工程判断是,不是越多 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} />;
}它更适合:
- 计算明显有成本
- 结果需要在依赖不变时复用
- 结果引用稳定本身也有价值
它不适合:
- 非常轻的计算
- 只是“看起来专业所以先加上”
3. useCallback 到底在缓存什么
useCallback 缓存的是函数引用。
jsx
function UserPage({ userId }) {
const handleRefresh = useCallback(() => {
console.log("refresh", userId);
}, [userId]);
return <Toolbar onRefresh={handleRefresh} />;
}它更适合:
- 回调会传给被
React.memo包裹的子组件 - 你确实需要一个稳定函数引用
- 某些依赖关系必须保持稳定
如果子组件本来也不会因为函数引用变化造成实际问题,那就没必要为了“优化”而强上。
4. React.memo 在解决什么问题
React.memo 作用在组件层。
它的核心目标是,当 props 没变时,跳过不必要的重复渲染。
jsx
const UserCard = React.memo(function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
{user.name}
</div>
);
});但要注意:
React.memo只对 props 比较有效- 默认比较是浅比较
- 如果每次都传新对象、新数组、新函数,它的收益可能很有限
所以很多时候它要和 useMemo、useCallback 一起看,而不是单独看。
5. useMemo、useCallback、React.memo 怎么串起来理解
可以这样分工:
| 能力 | 缓存什么 |
|---|---|
useMemo | 计算结果 |
useCallback | 函数引用 |
React.memo | 组件渲染结果是否可跳过 |
它们常见的配合方式是:
- 父组件用
useMemo保持对象结果稳定 - 父组件用
useCallback保持函数引用稳定 - 子组件用
React.memo减少重复渲染
但一定要先确认问题真的存在,再去做这层优化。
6. useReducer 适合什么场景
useReducer 适合状态更新逻辑比较复杂的场景。
例如:
- 同一块状态有很多种更新动作
- 更新规则需要集中管理
- 你希望把“状态”和“如何更新状态”拆开
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 | 浏览器绘制后 | 请求、订阅、一般副作用 |
useLayoutEffect | DOM 变更后、绘制前 | 读布局、同步改样式、避免闪动 |
例如:
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 默认更强调声明式数据流。
但某些场景下,父组件确实希望直接触发子组件暴露的方法,例如:
- 手动聚焦
- 打开弹层
- 重置内部表单
这时可以通过 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
它更适合把某些“不那么紧急”的状态更新标记成低优先级。
例如搜索框里:
- 输入框文本更新要立刻响应
- 大列表筛选可以稍微晚一点更新
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 更偏和“外部状态源”对接。
例如:
- 自己实现的小型状态库
- 浏览器 API 状态
- 不由 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 的主线,本质上是在解决 如何稳定复用状态逻辑、如何减少不必要的重复工作、如何在复杂场景下组织更新优先级,以及如何把少量特殊场景的命令式能力控制在边界之内。