这不是“百科式列表”,而是“能落地的心智模型”。每个 Hook 给你三样东西:它解决的痛点、何时用、易错点 + 最小示例。
面向小白的阅读指引:
- 你将学到什么
- 用“渲染驱动”的思维理解 React,而不是“生命周期函数”的背诵。
- 明白 Hooks 两条铁律、依赖数组该怎么写、为什么会出现“状态没更新/值不对”的问题。
- 看得懂最常用的 Hooks,并能在真实页面中组合使用。
- 前置准备
- 了解一点点 ES6(解构、箭头函数)即可;不会 TypeScript 也能读懂代码。
- 安装 React DevTools(浏览器扩展),方便调试组件树与状态。
- 心智模型(非常关键)
- 组件是“纯函数 + 状态”。每次渲染都会“重新执行函数”,得到新的 UI。
- Hooks 是“记忆与副作用的接口”。
useState记住值,useEffect处理渲染后的副作用。 - 任何 Hook 都和“渲染”绑定:依赖数组写谁,就代表这个 Hook 对谁“敏感”。
Hooks 两条规则(记住就不易踩坑):
- 只在组件最顶层调用 Hooks(不要写在 if、for 里)。
- 只在 React 函数组件或自定义 Hook 里调用 Hooks(不要在普通函数里随便用)。
渲染与 effect 的执行顺序(简化版):
- 触发一次状态更新(例如点击按钮)。
- React 重新执行组件函数 → 计算得到新的 JSX。
- 浏览器把变化渲染到页面。
- React 调用本次渲染对应的
useEffect回调;下次渲染前,会先运行上一次的清理函数(如果有)。
这解释了两个常见现象:
- 在一次渲染的事件处理函数里,读到的“状态值”就是这次渲染时的旧值(闭包特性),直到下一次渲染才会看到新值。
-
useEffect的回调总是发生在“渲染之后”,所以不要用它去“计算渲染要用的值”,而应该用它去“同步渲染带来的副作用”(如订阅、日志、网络请求等)。
目录
- 基础状态与副作用:useState / useEffect / useRef
- 性能与缓存:useMemo / useCallback
- 复杂状态:useReducer / useContext
- 并发与响应:useTransition / useDeferredValue
- 布局与命令式:useLayoutEffect / useImperativeHandle
- 生态与新能力:useId / useSyncExternalStore / useInsertionEffect / useOptimistic
- 自定义 Hook:抽象复用的正确姿势
- 常见误区与调试技巧
基础:useState / useEffect / useRef
痛点与场景:
- useState:组件内部可响应的状态。
- useEffect:对“渲染结果”做副作用(订阅、请求、DOM 接入等)。
- useRef:跨渲染保存可变值(不触发重渲染)或拿到 DOM。
示例:计数器 + 本地存储持久化
tsx
123456789101112131415161718
import { useEffect, useRef, useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(() => Number(localStorage.getItem('count') || 0));
const renders = useRef(0);
renders.current += 1;
useEffect(() => {
localStorage.setItem('count', String(count));
}, [count]);
return (
<div>
<p>Count: {count} (renders: {renders.current})</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
易错点:
- 不要在 effect 内直接更新“它自己的依赖状态”导致死循环;必要时用条件判断或将副作用与状态更新解耦。
- useRef 变化不会触发重渲染;适合保存“最新值”或外部实例。
性能:useMemo / useCallback
痛点:避免重复计算、稳定函数引用(传给子组件或依赖 hooks)。
示例:昂贵计算缓存与回调稳定
tsx
123456789101112131415161718192021222324
import { memo, useCallback, useMemo, useState } from 'react';
const Expensive = memo(function Expensive({ n, onPick }: { n: number; onPick: (v: number) => void }) {
const value = useMemo(() => {
// 模拟昂贵计算
let s = 0;
for (let i = 0; i < 5_000_00; i++) s += i % 10;
return s + n;
}, [n]);
return <div onClick={() => onPick(value)}>Value: {value}</div>;
});
export default function App() {
const [n, setN] = useState(1);
const [picked, setPicked] = useState<number | null>(null);
const onPick = useCallback((v: number) => setPicked(v), []);
return (
<div>
<input type="number" value={n} onChange={e => setN(Number(e.target.value))} />
<Expensive n={n} onPick={onPick} />
<p>picked: {picked}</p>
</div>
);
}
要点:
- useMemo/Callback 是“性能优化工具”,不是为了解决依赖警告;不要到处乱加。
- 只有当子组件依赖引用稳定性(memo/依赖数组)或计算开销大时再用。
复杂状态:useReducer / useContext
痛点:多个字段/复杂变更逻辑,或跨组件共享状态。
示例:Todo with reducer + context(就近状态 + 受控更新)
tsx
123456789101112131415161718192021222324
import { createContext, useContext, useReducer } from 'react';
type Todo = { id: number; text: string; done: boolean };
type Action = { type: 'add'; text: string } | { type: 'toggle'; id: number };
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
case 'toggle': return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
}
}
const TodosCtx = createContext<{ state: Todo[]; dispatch: React.Dispatch<Action> } | null>(null);
export function TodosProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, []);
return <TodosCtx.Provider value={{ state, dispatch }}>{children}</TodosCtx.Provider>;
}
export function useTodos() {
const ctx = useContext(TodosCtx);
if (!ctx) throw new Error('useTodos must be used within TodosProvider');
return ctx;
}
使用:
tsx
1234567891011121314151617181920
function Add() {
const { dispatch } = useTodos();
return <button onClick={() => dispatch({ type: 'add', text: 'learn hooks' })}>Add</button>;
}
function List() {
const { state, dispatch } = useTodos();
return (
<ul>
{state.map(t => (
<li key={t.id}>
<label>
<input type="checkbox" checked={t.done} onChange={() => dispatch({ type: 'toggle', id: t.id })} />
{t.text}
</label>
</li>
))}
</ul>
);
}
并发体验:useTransition / useDeferredValue
痛点:重计算或列表过滤卡顿;希望先让输入响应,慢任务稍后再做。
示例:搜索输入丝滑,列表慢慢算
tsx
123456789101112131415161718192021
import { useDeferredValue, useMemo, useState, useTransition } from 'react';
export default function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const deferred = useDeferredValue(query);
const list = useMemo(() => {
// 模拟大列表过滤
const data = Array.from({ length: 20000 }, (_, i) => `item-${i}`);
return data.filter(x => x.includes(deferred));
}, [deferred]);
return (
<div>
<input value={query} onChange={e => startTransition(() => setQuery(e.target.value))} />
{isPending && <span>Updating…</span>}
<p>Found: {list.length}</p>
</div>
);
}
要点:
- useTransition 让“状态更新”变成低优先级;useDeferredValue 让“值本身”延后参与计算。
布局与命令式:useLayoutEffect / useImperativeHandle
痛点:需要在浏览器绘制前读/写布局;或对外暴露有限的命令式 API。
示例:自动滚动 + 暴露滚动到顶方法
tsx
123456789101112131415161718
import { forwardRef, useImperativeHandle, useLayoutEffect, useRef } from 'react';
export type ListRef = { scrollToTop: () => void };
export const List = forwardRef<ListRef, { items: string[] }>(function List({ items }, ref) {
const el = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const node = el.current;
if (node) node.scrollTop = node.scrollHeight; // 初次渲染后滚动到底
}, [items.length]);
useImperativeHandle(ref, () => ({
scrollToTop() { el.current?.scrollTo({ top: 0, behavior: 'smooth' }); },
}), []);
return <div ref={el} style={{ maxHeight: 200, overflow: 'auto' }}>{items.map(i => <div key={i}>{i}</div>)}</div>;
});
生态与新能力:useId / useSyncExternalStore / useInsertionEffect / useOptimistic
- useId:SSR/CSR 一致的稳定 ID(表单/无障碍标签)。
- useSyncExternalStore:从外部 store 读取状态(如 Redux、Zustand 底层适配)。
- useInsertionEffect:在样式注入前同步执行(CSS-in-JS 库使用)。
- useOptimistic:对异步操作先行“乐观更新”,失败再回滚(表单提交等)。
示例:useId 关联 label/input
tsx
1234567891011
import { useId } from 'react';
export function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</div>
);
}
示例:useOptimistic(乐观 UI)
tsx
12345678910111213141516171819202122232425
import { useOptimistic, useState } from 'react';
async function save(name: string) {
await new Promise(r => setTimeout(r, 800));
return name.toUpperCase();
}
export default function Profile() {
const [name, setName] = useState('tom');
const [optimisticName, setOptimisticName] = useOptimistic(name);
async function submit() {
setOptimisticName(name + '...'); // 立即反馈
const saved = await save(name);
setName(saved);
}
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={submit}>Save</button>
<p>Preview: {optimisticName}</p>
</div>
);
}
自定义 Hook:抽象复用的正确姿势
两条铁律:
- 输入输出要稳定(参数、返回值类型清晰),隐藏实现细节。
- 用“第二次复用”作为抽象信号,不要过早通用化。
示例:useFetch(最小实现,演示取消与状态机)
tsx
1234567891011121314151617181920212223
import { useEffect, useRef, useState } from 'react';
type State<T> = { data?: T; error?: Error; loading: boolean };
export function useFetch<T>(url: string, init?: RequestInit) {
const [state, setState] = useState<State<T>>({ loading: true });
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
const ac = new AbortController();
abortRef.current = ac;
setState({ loading: true });
fetch(url, { ...init, signal: ac.signal })
.then(r => r.json())
.then((data: T) => setState({ data, loading: false }))
.catch(e => {
if (e.name !== 'AbortError') setState({ error: e, loading: false });
});
return () => ac.abort();
}, [url]);
return state;
}
常见误区与调试技巧
- 依赖数组怎么写:不是“想依赖谁就依赖谁”,而是“你的回调/效果闭包里读了谁就依赖谁”。用 ESLint 的
react-hooks/exhaustive-deps辅助检查。 - 只执行一次的 effect:多数用于“订阅/注册/初始化”。要返回清理函数,避免内存泄漏:
return () => unsubscribe()。 - 闭包与“读到旧值”:同一轮渲染内,事件处理函数捕获的是当时的值。要获取最新值可用
useRef保存,或用函数式更新:setCount(c => c + 1)。 - 何时不用 useEffect:派生数据请用
useMemo;由一次交互直接触发的逻辑放在“事件处理函数”里,不要放 effect 里。 - 受控表单:表单值存到
useState,用value/onChange控制;避免受控/非受控混用产生警告。 - 批量更新:React 会批处理同一 tick 内的多次
setState;依赖前一个值时用函数式更新,防止丢更新。 - DevTools 调试:组件树中选中节点可查看 Hooks 状态;Profiler 面板能看渲染耗时,定位不必要的重新渲染。
- Strict Mode:开发环境可能触发“双执行”以帮助发现副作用问题;不要据此判断生产行为。
新手速查 FAQ(遇到就翻)
- setState 为什么没立刻更新?
- 更新是“异步、批量”的;本次渲染中读到的是旧值。需要基于旧值更新时用函数式写法:
setX(x => x + 1)。
- 更新是“异步、批量”的;本次渲染中读到的是旧值。需要基于旧值更新时用函数式写法:
- useEffect 依赖该怎么写?
- 让 ESLint 插件告诉你。回调里读了谁,就把谁放进依赖;若不想触发,通常是设计要调整,而非删依赖。
- useEffect 和 useMemo 有啥区别?
- useMemo 是“渲染期的纯计算缓存”;useEffect 是“渲染后做副作用”。不要混用。
- 请求该放哪里?
- 客户端组件:可以放
useEffect,注意竞态取消;Next App Router 推荐服务端数据获取优先。
- 客户端组件:可以放
- 我需要全局状态吗?
- 优先用组件就近状态;跨层传递再考虑 Context;更复杂再看外部状态库(Zustand/Redux)。
小结
- Hooks 是“渲染驱动”模型的拼装件:状态、派生、效果、并发、边界。
- 先理解痛点与心智模型,再按需挑选;不要用 Hook 掩盖设计问题。
- 在真实项目里,配合类型、状态机、数据层抽象,Hooks 会更“稳”。