React 语法速查表 - React 常用 Hooks 与组件开发大全
这张速查面向以函数组件和 Hooks 为主业的 React 开发者。内容从 useState/useEffect 的状态与副作用核心讲起,过渡到 useReducer、Context、useId 等进阶 Hooks,再讲 forwardRef、render props 等组件模式,接着是受控表单与基于 Context 的状态共享,最后落到 React.memo、懒加载和 Suspense 这类性能优化。每条都是可直接复制的片段,并标注好依赖数组与边界情况。读完你应能用 useEffect 驱动异步请求、用 Context 跨组件共享状态,并通过 memo/useCallback 减少不必要的重渲染。
典型使用场景
面向从 class 组件迁移到函数组件、或接手一个 hooks 写得很随意的 React 项目的开发者。真正决定代码质量的不是「会用几个 hook」,而是三件事:状态放在哪一层、副作用何时执行、重渲染如何收敛。本表覆盖 useState / useEffect / useRef / useMemo / useCallback / useReducer / useContext 这一组核心 hook,以及条件渲染、列表 key、错误边界、Portal、懒加载这些每天都会撞上的写法。适合在 code review 之前对照检查一遍「这个状态该不该提到父组件」「这个 effect 有没有清理函数」。
Hooks 基础 6
const [count, setCount] = useState(0)const [user, setUser] = useState({ name: "", age: 0 })useEffect(() => { fetchData() }, [])useEffect(() => { subscribe(id); return () => unsubscribe() }, [id])const ref = useRef(null)const memoized = useMemo(() => compute(a, b), [a, b])Hooks 进阶 6
const [state, dispatch] = useReducer(reducer, initialState)const value = useContext(MyContext)useLayoutEffect(() => { measure() }, [deps])useImperativeHandle(ref, () => ({ focus: () => {} }))const id = useId()const [isPending, startTransition] = useTransition()组件模式 6
function App({ name, children }) { return <div>{children}</div> }const Button = forwardRef((props, ref) => <button ref={ref} />)function withAuth(Component) { return (props) => <Component {...props} /> }function List({ render }) { return items.map(render) }const Memo = React.memo(Component)const Lazy = React.lazy(() => import("./Comp"))事件与表单 5
<button onClick={(e) => handleClick(e)}>Click</button><input value={val} onChange={e => setVal(e.target.value)} /><form onSubmit={e => { e.preventDefault(); submit() }}>e.stopPropagation()e.preventDefault()状态管理 5
setCount(prev => prev + 1)setUser(prev => ({ ...prev, name: "new" }))const ThemeContext = createContext("light")<ThemeContext.Provider value="dark">const value = useContext(ThemeContext)性能优化 5
const MemoComp = React.memo(MyComponent)const value = useMemo(() => expensive(a, b), [a, b])const handler = useCallback(() => {}, [deps])<Suspense fallback={<Loading />}><Lazy /></Suspense>const [deferred] = useDeferredValue(value)常用模式 6
{isLoading ? <Loading /> : <Content />}{show && <Menu />}{items.map(item => <li key={item.id}>{item.name}</li>)}class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} }ReactDOM.createPortal(<Modal />, document.body)<Comp {...defaultProps} {...props} />参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
useState(initial) | 声明组件状态,返回 [当前值, 更新函数] | const [count, setCount] = useState(0) |
setX(prev => next) | 基于前值更新,避免闭包读到旧值 | setCount(prev => prev + 1) |
useEffect(fn, deps) | 挂载后 / 依赖变化后执行副作用,deps 为空数组则只跑一次 | useEffect(() => { fetchData() }, []) |
useRef(null) | 返回跨渲染保留的可变对象,可挂 DOM 节点或存非渲染值 | const ref = useRef(null) |
useMemo(fn, deps) | 依赖不变时复用上次计算结果,避免昂贵计算重复执行 | const memoized = useMemo(() => compute(a, b), [a, b]) |
useCallback(fn, deps) | 记忆回调函数本身,配合 React.memo 阻止子组件重渲染 | const handler = useCallback(() => {}, [deps]) |
React.memo(Component) | 浅比较 props,相同则跳过子组件重渲染 | const Memo = React.memo(Component) |
key={item.id} | 列表渲染的稳定标识,缺失或用索引会导致状态错位 | {items.map(item => <li key={item.id}>{item.name}</li>)} |
<Suspense fallback={<Loading />}> | 懒加载组件加载期间的兜底 UI,必须与 React.lazy 搭配 | <Suspense fallback={<Loading />}><Lazy /></Suspense> |
getDerivedStateFromError(e) | 错误边界的静态方法,捕获子组件渲染错误 | class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} } |
易错点与避坑指南
现象列表在删除/排序后出现「串行」现象:某一项显示了下一项的输入内容或展开状态。
原因key 用了数组索引或干脆没写,React 只能按位置复用 DOM,状态跟着位置走。
处置改为稳定唯一标识:{items.map(item => <li key={item.id}>{item.name}</li>)};若本来就没有 id,先在数据源补一个唯一字段。
现象useEffect 里的请求发起两次,开发环境下日志打印两遍请求。
原因StrictMode 在开发环境会挂载 → 卸载 → 再挂载一次以暴露副作用问题,effect 被执行两遍。
处置先确认请求是否需要幂等或加清理;真正的修复是在 effect 里写好取消逻辑(AbortController 或 return 清理函数),而不是关闭 StrictMode。
现象事件回调里读到的 state 永远是第一次渲染时的那个值,日志打印「旧数据」。
原因回调函数在渲染时被创建并被缓存,闭包捕获了那一次渲染的 state,后续更新不会刷新这个闭包。
处置把状态放进依赖数组,或改用函数式更新:setCount(prev => prev + 1) / setUser(prev => ({ ...prev, name: "new" })),让更新基于最新值。
现象组件卸载后定时器/订阅还在跑,控制台报 setState on unmounted 或内存持续上涨。
原因useEffect 注册了订阅或 setInterval,但没有 return 清理函数。
处置在 effect 里返回清理函数:useEffect(() => { subscribe(id); return () => unsubscribe() }, [id]),并在 id 变化时先清旧订阅再订新。
现象对象或数组状态改了字段,界面完全不更新。
原因直接修改了原对象(state.count++ 这类写法),引用没变 React 不会重渲染。
处置一律做不可变更新:setUser(prev => ({ ...prev, name: "new" })),或用数组方法返回新数组。
现象把 useEffect 当成生命周期钩子用,写了多个互相拆台的 effect,逻辑互相覆盖。
原因在渲染期间做本该在提交后做的清理,或同一个副作用拆成两段互相覆盖的注册。
处置按「读取 DOM / 订阅 / 请求」分类各写一个 effect,每个自己带清理;DOM 测量用 useLayoutEffect。
排障路径
1确认某段逻辑到底跑了几次
useEffect(() => { console.log("effect run") }, [])在 effect 里打点日志;开发环境出现两次通常是 StrictMode 的挂载-卸载-再挂载,只要清理逻辑写对就不影响生产。
2定位是哪一次渲染创建了过期闭包
const handler = useCallback(() => {}, [deps])回调里引用的每个外部变量都应出现在 deps 中;用 useCallback 固定引用,再配合 React.memo 让子组件跳过重渲染。
3把重渲染限制在必要范围内
const MemoComp = React.memo(MyComponent)先用 React DevTools Profiler 确认哪些组件在无谓重渲染,再对症加 React.memo / useMemo / useCallback,而不是到处包。
4按需渲染与列表渲染的最小正确写法
{isLoading ? <Loading /> : <Content />}条件渲染用三元或短路表达式;列表渲染必须带稳定 key,二者都要出现在本表的渲染分组里。
5卸载时确保副作用被收回
useEffect(() => { subscribe(id); return () => unsubscribe() }, [id])所有订阅、定时器、请求监听都应有对应的清理回调;缺了这行就是内存泄漏的起点。
命令示例
带清理的副作用,id 变化时重建订阅
useEffect(() => {
subscribe(id);
return () => unsubscribe();
}, [id])依赖数组不是「优化」而是「订阅清单」:id 从 1 变成 2 时,React 先执行上一次返回的清理函数,再执行新的副作用。没有 return () => unsubscribe() 的话,旧的订阅仍然活着,组件在多个 id 之间来回切换就会叠加出多条连接——这类问题在开发模式的 StrictMode 双调用下会更快暴露,但生产环境里表现为偶发的重复推送。
基于前值更新,避免闭包捕获旧 state
setCount(prev => prev + 1)
setUser(prev => ({ ...prev, name: "new" }))同一事件处理器里连调三次 setCount(count + 1),三次读到的都是本次渲染的 count,结果只加了 1;写 setCount(prev => prev + 1) 才能基于最新值累加。对象状态同样不能直接 setUser({ name: "new" }),那会整体替换掉 age 字段——展开 prev 保留其余字段。
列表 key 用稳定 id,不要用数组下标
{items.map(item => <li key={item.id}>{item.name}</li>)}
{items.map((item, i) => <li key={i}>{item.name}</li>)}key 告诉 React 哪个 DOM 节点对应哪条数据。用下标时,在列表头部插入一条会让后面所有 key 整体位移,React 误以为第 1 项变成了第 2 项,于是把已有节点的文本原地改掉——输入框会保留旧的输入内容、动画错乱。只有列表永不重排、不增删且纯展示时,下标才可接受。
受控表单:value 与 onChange 必须成对
const [val, setVal] = useState("")
<input value={val} onChange={e => setVal(e.target.value)} />
<form onSubmit={e => { e.preventDefault(); submit() }}>只写 value 不写 onChange,输入框会完全无法编辑(React 报错 "You provided a value prop to a form field without an onChange handler"),而只写 onChange 不写 value 则是非受控组件。表单提交必须 e.preventDefault(),否则页面会刷新导致状态丢失。
useMemo / useCallback 配 React.memo 才有意义
const memoized = useMemo(() => compute(a, b), [a, b])
const handler = useCallback(() => {}, [deps])
const Memo = React.memo(Component)
<Suspense fallback={<Loading />}><Lazy /></Suspense>子组件被 React.memo 包住时,父组件每次渲染都会传新的 props 对象和新的函数,memo 就完全失效。用 useMemo 稳定 props、useCallback 稳定回调,memo 才拦得住重渲染。注意这两者都要带依赖数组且依赖写全,漏依赖会读到旧值(比多余依赖更难查)。
常见坑与注意事项
- 闭包捕获旧 state 是 React 里最难查的一类 bug:useEffect 或事件回调里直接读 state,得到的是那次渲染时的值,setState 之后回调仍在用旧值。典型症状是 setInterval 或 SSE 回调永远拿到第一次的 id。解法是把值放进依赖数组(useEffect([id]))或用 setX(prev => ...) 函数式更新。
- useEffect 依赖数组漏写是错误的常见来源。eslint 的 exhaustive-deps 会报出来,但很多人习惯性加 eslint-disable——漏掉依赖后副作用不会在需要时重跑,订阅了旧的回调引用,或者每次渲染都重建定时器却因为省略依赖而只执行一次。依赖写不全比依赖写多的破坏性更大。
- 列表 key 用数组下标(key={i})在插入、删除、排序时会让 React 复用错误的 DOM 节点,输入框内容错位、组件内部 state 跑到别的行上。凡是数据带 id 就必须用 id;确实没有稳定 id 时,先在数据层生成再渲染。
- useEffect 忘记写 return () => unsubscribe() / clearInterval / abort():组件卸载后定时器、订阅、请求仍在跑,内存持续增长,页面来回切换几次后出现重复请求与卡顿。React.memo 或条件渲染让组件频繁挂载卸载时问题被放大。
- 直接修改 state 对象(user.name = "x"; setUser(user))绕过 React 的不可变约定:引用没变,React 认为没有变化而跳过重渲染,界面停留在旧值。对象和数组状态一律用展开 prev => ({ ...prev, x }) 或 [...prev, item] 产生新引用。
- 把构建期环境变量直接写进组件里:SSR 与 CSR 两侧取值不一致时,hydration 阶段客户端渲染出的文本与服务端不同,控制台报 hydration mismatch,页面局部闪烁。统一走服务端下发的配置对象或在模块顶层集中读取一次再传下去,不要在组件里散落环境变量的字面量写法。
提示
- useEffect 的依赖数组为空 [] 时只在挂载时执行一次,类似 componentDidMount。
- useMemo 和 useCallback 用于性能优化,避免不必要的重新计算和渲染。
- React 18 的 StrictMode 在开发模式下会双重调用 useEffect,帮助发现副作用问题。
- Suspense + React.lazy() 可以实现组件懒加载和代码分割。
- 列表渲染必须为每个元素提供稳定的 key,避免用数组 index 作为 key。
常见问题
useRef 和 useState 该怎么选?
判断标准是「这份数据要不要驱动界面」。要显示在屏幕上、变化后必须重渲染,就用 const [count, setCount] = useState(0) 这一对。不要显示、或者改动太频繁不值得触发渲染,就用 const ref = useRef(null):它返回一个可变的 ref 对象,既能挂 DOM 节点让父组件拿到真实元素,也能存一个跨渲染不会丢的值而不会引发任何重渲染。典型分界是输入框的受控值走 useState,而计时器 id、滚动位置、是否已经初始化这类内部状态走 useRef。
useTransition 和 useDeferredValue 有什么区别?
两者都是把「不紧急」的更新推迟,好让用户输入立刻有反馈,但出发点不同。const [isPending, startTransition] = useTransition() 是你主动声明一次更新属于非紧急:把 setState 放进 startTransition 里,isPending 会在过渡期间为 true,你可以拿它驱动加载指示;输入框这类高频更新不受影响。const [deferred] = useDeferredValue(value) 是被动跟随:你照常正常更新 value,React 额外给出一个滞后版本去渲染次要区域——比如搜索结果列表,让它晚一点刷新而输入框不卡。简单说:前者由你标记哪次更新重要,后者由 React 自动派生一个落后值。
forwardRef 和 useImperativeHandle 分别在什么场景用?
两者解决的是同一个问题的两种粒度。const Button = forwardRef((props, ref) => <button ref={ref} />) 是整体层面的转发:它把父组件拿到的 ref 原样塞进内部某个 DOM 元素,父组件拿到的是那个真实节点,可以 focus、测量尺寸、加样式。useImperativeHandle(ref, () => ({ focus: () => {} })) 是更精细的收口:你决定暴露什么,函数返回的对象就是父组件通过 ref 能看到的全部成员,内部实现可以随便重构而调用方不受影响。简单转发用 forwardRef,需要自定义一组方法或屏蔽内部细节时用 useImperativeHandle。
跨层级共享数据时,Context、高阶组件和 props 该怎么选?
按共享的范围和变化频率来分。props 传 children 是最局部的方式,父组件写 function App({ name, children }) { return <div>{children}</div> } 把内容包进来,数据只在这一层子树里可见,改动只影响直接子组件。const ThemeContext = createContext("light") 加上 <ThemeContext.Provider value="dark"> 和 const value = useContext(ThemeContext) 适合主题、语言这类全局值:任意深度都能取到,但 Provider 的 value 一变,所有消费者都会重渲染。function withAuth(Component) { return (props) => <Component {...props} /> } 这类高阶组件介于两者之间,适合横切注入鉴权、日志这类行为,而不只是数据。
错误边界能捕获哪些错误,捕获不到哪些?
本表里的 class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} } 是类组件写法,靠 static getDerivedStateFromError 在渲染阶段的错误冒泡到它时接住并设置错误状态。它能捕获的是子组件在渲染、构造函数以及生命周期里抛出的错误,覆盖不到的是事件处理器里的异常、setTimeout 或 Promise 回调里的异常,以及它自己内部抛出的错误——这些都不在 React 的渲染调用栈上。SSR 阶段和错误边界自身渲染时的错误同样不在覆盖范围内。异步与事件回调里的失败需要自己在数据层捕获并转成状态。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。