useState 内置 函数签名 const [state, setState] = useState<T>(initial: T | (() => T))复制
最常用的状态 hook。返回 [当前值, setter] 元组。setter 在多次渲染之间引用稳定,调用它会触发用新值重新渲染。
✓ 何时用
组件内任何"改变要触发重渲染"的值 —— 表单输入、开关、计数器、拉到的数据、UI 状态。
✗ 何时不用
从已有 state 派生出来的值(直接算或用 useMemo 即可)。不需要触发重渲染的引用值(用 useRef)。多种状态转换交织的复杂状态(用 useReducer)。
⚠ 常见坑: 闭包陷阱:连续两次 setState(count + 1) 只会 +1 一次,因为两次读到的 count 是同一个值。下一个值依赖上一个时,必须用函数式写法 setState(c => c + 1)。
例子 const [count, setCount] = useState(0);
// functional form is mandatory when next depends on prev
const inc = () => setCount(c => c + 1); // lazy initializer — fn runs once, not on every render
const [grid, setGrid] = useState(() => buildExpensiveGrid(1000));
useReducer 内置 函数签名 const [state, dispatch] = useReducer<R>(reducer, initialArg, init?)复制
适合"多种动作切换同一个状态"的写法。reducer 是 (state, action) => state 的函数。dispatch 引用稳定,传给子组件不会破坏 memo。
✓ 何时用
5 个以上字段的表单。idle/loading/success/error 多状态的异步流程。任何在一个 handler 里要连写多个相关 setState 的场景。
✗ 何时不用
单个布尔或计数器(useState 更短)。只在一个组件里、状态转换不超过两种的(useState 更少代码)。
⚠ 常见坑: reducer 必须是纯函数 —— 里面不能有 fetch / setTimeout / 操作 DOM。副作用要放到 useEffect 或调 dispatch 的那个 handler 里。
例子 type Action = { type: 'inc' } | { type: 'reset' };
function reducer(state: number, action: Action) {
switch (action.type) {
case 'inc': return state + 1;
case 'reset': return 0;
}
}
const [count, dispatch] = useReducer(reducer, 0); // async flow modeled as a state machine
type S = { status: 'idle' | 'loading' | 'ok' | 'err'; data?: User };
type A = { type: 'start' } | { type: 'ok'; data: User } | { type: 'err' };
useEffect 内置 函数签名 useEffect(effect: () => (void | (() => void)), deps?: any[])复制
在浏览器绘制 之后 跑副作用。可选的清理函数会在下一次 effect 之前以及卸载时跑。依赖数组控制 effect 何时重新触发。
✓ 何时用
订阅外部系统(WebSocket、事件监听、observer)。同步非 React 状态(localStorage、document.title)。任何要在 DOM 更新 之后 响应 props/state 变化的事。
✗ 何时不用
从 props 派生 state(直接算就行,不用 effect)。每次都跑、不带清理的(根本不需要 effect,函数体里写就行)。props 变了想重置 state(用 `key` 属性或渲染期对比上次值)。
⚠ 常见坑: 依赖数组漏了 = 闭包陷阱:effect 抓的是它被 定义 时那一渲染的值。一定要打开 `eslint-plugin-react-hooks` 的 exhaustive-deps,把读到的每个值都列进去,或者把值挪到 ref 里、挪到组件外。
例子 useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id); // cleanup is mandatory for timers
}, []); // subscribe + cleanup pattern
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []); // data fetching with abort to avoid setState-after-unmount warning
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/user/${id}`, { signal: ctrl.signal })
.then(r => r.json()).then(setUser).catch(() => {});
return () => ctrl.abort();
}, [id]);
useLayoutEffect 内置 函数签名 useLayoutEffect(effect: () => (void | (() => void)), deps?: any[])复制
签名和 useEffect 一样,但在 DOM 更新之后、浏览器绘制 之前 同步触发。用来量尺寸或改布局可以避免视觉抖动。
✓ 何时用
量 DOM 尺寸,然后在同一帧里定位另一个元素。读滚动位置。同步修正布局,防止用户看到错的中间态闪一下。
✗ 何时不用
不需要"同一帧视觉一致"的事都不要用。重活(会卡住绘制、掉帧)。SSR —— useLayoutEffect 在服务器不跑,React 会警告。
⚠ 常见坑: 它会阻塞绘制。里面做重活就卡顿。SSR 时收到 "useLayoutEffect does nothing on the server" 警告,要么换 useEffect,要么把它隔离起来。
例子 useLayoutEffect(() => {
const { height } = ref.current!.getBoundingClientRect();
setRowHeight(height); // setState is safe — it batches before paint
}, []);
useInsertionEffect 内置 函数签名 useInsertionEffect(effect: () => (void | (() => void)), deps?: any[])复制
面向 CSS-in-JS 库的小众 hook。在任何 DOM 变更 之前 同步触发,让库可以注入 <style> 标签而不引发布局抖动。
✓ 何时用
几乎不在业务代码里用。设计目标是 styled-components / Emotion / Stitches 这类库,在 React commit 前插入生成的样式表。
✗ 何时不用
业务代码几乎一定该用 useEffect 或 useLayoutEffect。在这里读布局是错的 —— DOM 还没变更。这阶段 ref 也还没挂上。
⚠ 常见坑: ref 还没挂,而且这里不能 setState —— React 官方文档明说。当它是库专用 hook 就行。
例子 // styled-components-style use case
useInsertionEffect(() => {
const tag = document.createElement('style');
tag.textContent = generatedCss;
document.head.appendChild(tag);
return () => tag.remove();
}, [generatedCss]);
useMemo 内置 函数签名 const value = useMemo<T>(() => compute(), deps)复制
在多次渲染之间缓存一个昂贵计算的返回值。只有依赖数组里的值变化时,函数才重新跑。
✓ 何时用
昂贵的纯计算(长列表排序 + 过滤、复杂解析)。给 memo 子组件或者其他 hook 的依赖数组,提供引用稳定的对象/数组。
✗ 何时不用
便宜的计算 —— useMemo 自己也有开销。只是"加上心里踏实"的场合都别加。给所有东西套一层是公认的反模式,只让 bundle 变大、性能更差。
⚠ 常见坑: useMemo 是"建议",不是"保证" —— React 可以为释放内存而丢弃缓存。绝不能把"只能跑一次"的逻辑放进去;要保证只跑一次用 useEffect([]) 或 ref。
例子 const visible = useMemo(
() => items.filter(i => i.name.includes(query)).sort(byDate),
[items, query],
); // stabilize an object so memoized child does not re-render
const style = useMemo(() => ({ color, fontSize }), [color, fontSize]);
useCallback 内置 函数签名 const fn = useCallback<T>(callback, deps)复制
在多次渲染之间缓存函数引用,让 React.memo 的子组件或下游 hook 拿到稳定的回调。等价于 useMemo(() => fn, deps)。
✓ 何时用
把 handler 传给 React.memo / PureComponent 的子组件。把函数放进另一个 hook 的依赖数组。除此之外 —— 别用。
✗ 何时不用
没有 memo 的子组件用了没用(反正都会重渲染)。给所有 handler 都套一层和给所有值套 useMemo 一样,是反模式。
⚠ 常见坑: 和 useEffect 一样的闭包陷阱:漏依赖会让缓存的函数读到 旧 值。必须开 exhaustive-deps lint。
例子 const onSelect = useCallback(
(id: string) => setSelected(s => ({ ...s, [id]: true })),
[], // setSelected from useState is already stable
);
useRef 内置 函数签名 const ref = useRef<T>(initial: T): { current: T }复制
返回一个可变的 .current 对象,跨渲染保留但 不会 触发重渲染。两种用法:(1) 通过 ref 属性拿 DOM 节点,(2) 当作类的实例变量存可变值。
✓ 何时用
读或聚焦 DOM 节点。存定时器 id、上一次的值、不能触发重渲染的标志位。存可变缓存。
✗ 何时不用
UI 需要响应的状态 —— 用 useState。ref 不参与协调,改 .current 不会触发重渲染。
⚠ 常见坑: 渲染期读 ref.current 很脆 —— DOM ref 在首次渲染时是 null。要在 useEffect / useLayoutEffect 或事件 handler 里读,别在渲染函数里读。
例子 const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />; // instance value pattern — no re-render on change
const lastSeenRef = useRef(0);
function onScroll() { lastSeenRef.current = window.scrollY; }
useImperativeHandle 内置 函数签名 useImperativeHandle<T, R extends T>(ref, () => R, deps?)复制
自定义通过 ref 暴露给父组件的值。要和 forwardRef 配合。用来暴露窄接口(如 { focus, scrollIntoView }),不直接给原始 DOM。
✓ 何时用
可复用的输入组件,向父级暴露 focus() 和 clear()。模态框暴露 open()/close()。任何被封装的组件,父级需要少量命令式 API。
✗ 何时不用
能用 props 声明式表达的就别用 —— 优先 props。要暴露整个 DOM 节点的话,直接 forwardRef 转发就够了。
⚠ 常见坑: 不套 forwardRef,函数组件上的 ref 等于无效。漏写依赖数组,每次渲染都会重建那个命令式对象。
例子 const Input = forwardRef<{ focus: () => void }, Props>((props, ref) => {
const inner = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inner.current?.focus(),
}), []);
return <input ref={inner} {...props} />;
});
useContext 内置 函数签名 const value = useContext<T>(Context)复制
在函数组件里读 Context 的值。上面最近的 Provider 推新值时,组件就会重渲染。
✓ 何时用
横切关注点:主题、当前用户、i18n、特性开关。子树里很多组件都要、又不想一级级传 prop 的值。
✗ 何时不用
频繁变化的值(每个消费者都会重渲染)。要细粒度更新别用 context 顶替状态库 —— 用 Zustand / Jotai / Redux 的 selector。
⚠ 常见坑: Provider 的 value 直接写对象字面量,每次都新建一个,所有消费者全部重渲染。用 useMemo 包一下:<Ctx.Provider value={useMemo(() => ({a, b}), [a, b])}>。
例子 const ThemeCtx = createContext<'light' | 'dark'>('light');
function Toolbar() {
const theme = useContext(ThemeCtx);
return <div className={theme}>...</div>;
}
useTransition 内置 函数签名 const [isPending, startTransition] = useTransition()复制
把一次 state 更新标记为非紧急。React 会让旧 UI 保持可交互,后台渲染过渡用的新 UI,isPending 告诉你正在进行。
✓ 何时用
边输入边过滤长列表。切换 tab,新 tab 渲染很贵的场景。任何渲染慢导致输入/点击发卡的地方。
✗ 何时不用
用户期望立刻反映的更新(输入框值、按钮切换)。副作用 —— startTransition 是给 state 更新用的,不要拿来包 fetch / 定时器。
⚠ 常见坑: 拿它包 fetch 没用 —— 它只把回调里的 state 更新 延后。数据请求该用 Suspense + use(),useTransition 包随后那次 state 更新。
例子 const [isPending, startTransition] = useTransition();
function onChange(e: ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value); // urgent: input updates now
startTransition(() => setList(filter(e.target.value))); // non-urgent
}
useDeferredValue 内置 函数签名 const deferred = useDeferredValue<T>(value)复制
返回一个延迟版的值。在昂贵渲染期间,这个延迟版会比真值"慢一拍",让紧急更新先生效。类似 useTransition,但用在你拿不到 setter 的值上(比如 props)。
✓ 何时用
收到一个频繁变化的 prop,要用它渲染一个昂贵的视图但不想阻塞输入。配 React.memo 用在那个慢子组件上,只有延迟版变化才触发慢渲染。
✗ 何时不用
你拿得到 setter 时,useTransition 更明确。极小的值,推迟反而闪烁、性能也没好处。
⚠ 常见坑: 昂贵的消费组件没套 React.memo,useDeferredValue 还是会触发慢渲染,只是晚一点。memo 才是真正让它生效的关键。
例子 function Search({ query }: { query: string }) {
const deferred = useDeferredValue(query);
const isStale = deferred !== query;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<HeavyList query={deferred} />
</div>
);
}
useSyncExternalStore 内置 函数签名 const snapshot = useSyncExternalStore<T>(subscribe, getSnapshot, getServerSnapshot?)复制
订阅外部(非 React)数据源 —— Redux store、浏览器 API、observable —— 并且在并发渲染下不撕裂。返回当前快照,store 通知时触发重渲染。
✓ 何时用
为非 React 的数据源(window.matchMedia、document.visibilityState、第三方状态管理库)写自定义 hook。写库的人做并发安全的绑定。
✗ 何时不用
普通组件 state(useState)或基于 context 的 store(useContext)。大部分业务代码用不到 —— 它是底层水管。
⚠ 常见坑: getSnapshot 在没变化时必须返回 引用相同 的值 —— 每次都新建对象会触发无限重渲染。在闭包里缓存快照。
例子 function useOnline() {
return useSyncExternalStore(
(cb) => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine,
() => true, // SSR snapshot
);
}
useId 内置 函数签名 const id = useId(): string复制
生成一个唯一、确定的 id,服务端和客户端渲染结果一致。设计目的是无障碍属性,把 label 和 input 关联起来。
✓ 何时用
aria-labelledby、可复用表单组件里的 htmlFor + id、tooltip 上的 aria-describedby。任何需要"在 SSR 下也一致"的唯一 id。
✗ 何时不用
列表的 key(用数据自己的 id)。CSS 选择器或 DOM 查找用的 id —— 返回的字符串包含 :,做 CSS 选择器要转义。
⚠ 常见坑: id 里带冒号(类似 `:r1:`)—— 当 HTML 属性没问题,但 querySelector(`#${id}`) 会挂。改用 document.getElementById,它接受任何字符串。
例子 function Field({ label }: { label: string }) {
const id = useId();
return <><label htmlFor={id}>{label}</label><input id={id} /></>;
}
useOptimistic 内置 函数签名 const [optimistic, addOptimistic] = useOptimistic<T, A>(state, updateFn)复制
React 19+。在 server action 还在路上时,先把乐观结果展示给用户。返回基于真实 state + 待处理乐观更新算出的临时 state,action 完成后自动对齐。
✓ 何时用
点赞 / 投票按钮。发聊天消息。切换设置项,服务端确认很快但 UI 要 0 延迟反馈。
✗ 何时不用
服务端慢、容易失败的请求 —— 回滚闪一下比直接转圈更难看。乐观值会被服务端改写的场景(服务器会规范化输入)。
⚠ 常见坑: 必须在 transition 或 server action 里调用 —— 普通事件 handler 里调没视觉效果。action 一完成乐观值就消失;想保留要同步到本地 state。
例子 const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, _: number) => current + 1,
);
async function onLike() {
addLike(1); // UI updates immediately
await serverLike(postId); // real call
}
useActionState 内置 函数签名 const [state, action, isPending] = useActionState<S, P>(reducerFn, initial, permalink?)复制
React 19+。管理"通过异步 action 切换"的 state —— 典型场景是表单提交。把 reducer 风格的更新、自动 pending 标志、表单的渐进增强组合在一起。
✓ 何时用
带服务端校验的表单提交(如登录表单返回 {error: "密码错"})。任何想要"自动 pending state"的异步切换,省得手写 useState + try/finally。
✗ 何时不用
同步的 state 变化(useState / useReducer 更简单)。不像表单的异步流程 —— useTransition 更灵活。
⚠ 常见坑: reducer 签名是 (state, formData) => Promise<state>,不是经典的 (state, action)。action 还没完用户就跳走,isPending 还是 true —— 在意的话在 effect 里清理。
例子 async function login(prev: State, fd: FormData): Promise<State> {
const res = await api.login(fd.get('email'), fd.get('password'));
return res.ok ? { user: res.user } : { error: res.message };
}
const [state, action, isPending] = useActionState(login, { user: null });
return <form action={action}>...</form>;
useFormStatus 内置 函数签名 const { pending, data, method, action } = useFormStatus()复制
React 19+。读取 父级 <form> 的提交状态。设计目的是在表单 内部 的按钮/输入框里调用,自己 disable 或显示 spinner,不用一级级传 prop。
✓ 何时用
可复用的 <SubmitButton>,表单提交时自动 disable。字段级别的内联进度提示,不需要知道表单具体怎么实现。
✗ 何时不用
不在表单里调(返回 { pending: false, data: null })。已经有 useActionState 的 isPending 时,直接传下去就行。
⚠ 常见坑: 组件不在 form 内部时,返回 false / null。和 <form> 同级的组件里调,永远是 false —— 它读的是 父级 form,不是当前组件挂的那个。
例子 function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>
{pending ? 'Saving…' : 'Save'}
</button>;
}
use 内置 函数签名 const value = use<T>(promise | Context)复制
React 19+。统一的"读"原语 —— 传 Promise 会挂起直到 resolve,传 Context 会读它的值。和别的 hook 不一样,use() 可以在 if / 循环 / 提前 return 里调。
✓ 何时用
在 Suspense 边界里读 server-action 的 Promise(React 19 渲染异步数据的标准方式)。条件式读 context(useContext 不能放在条件里)。
✗ 何时不用
在组件函数体里 当场 new Promise —— 每次渲染都是新 Promise,会无限挂起。Promise 要从父级传进来,或者 memo 住。
⚠ 常见坑: use(somePromise) 读的是 缓存的 promise。如果不稳定引用(在父级稳定、用 cache()、或交给 React Query),会引发 挂起→解决→重渲染 的死循环。
例子 function User({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends until resolved
return <div>{user.name}</div>;
}
// conditional context read — useContext cannot do this
const value = condition ? use(CtxA) : use(CtxB);