React 语法速查表 - React 常用 Hooks 与组件开发大全

这张速查面向以函数组件和 Hooks 为主业的 React 开发者。内容从 useState/useEffect 的状态与副作用核心讲起,过渡到 useReducer、Context、useId 等进阶 Hooks,再讲 forwardRef、render props 等组件模式,接着是受控表单与基于 Context 的状态共享,最后落到 React.memo、懒加载和 Suspense 这类性能优化。每条都是可直接复制的片段,并标注好依赖数组与边界情况。读完你应能用 useEffect 驱动异步请求、用 Context 跨组件共享状态,并通过 memo/useCallback 减少不必要的重渲染。

编程语言·共 39 条命令·最后更新 2026-07-21

典型使用场景

面向从 class 组件迁移到函数组件、或接手一个 hooks 写得很随意的 React 项目的开发者。真正决定代码质量的不是「会用几个 hook」,而是三件事:状态放在哪一层、副作用何时执行、重渲染如何收敛。本表覆盖 useState / useEffect / useRef / useMemo / useCallback / useReducer / useContext 这一组核心 hook,以及条件渲染、列表 key、错误边界、Portal、懒加载这些每天都会撞上的写法。适合在 code review 之前对照检查一遍「这个状态该不该提到父组件」「这个 effect 有没有清理函数」。

Hooks 基础 6

const [count, setCount] = useState(0)
useState 返回 [值, 更新函数],调用 setCount 会触发组件重渲染
const [user, setUser] = useState({ name: "", age: 0 })
对象状态的初始值;更新时注意用展开保持不可变
useEffect(() => { fetchData() }, [])
依赖数组为空,只在组件挂载后执行一次,适合拉取初始数据
useEffect(() => { subscribe(id); return () => unsubscribe() }, [id])
带清理回调的副作用,id 变化或卸载时先执行 return 清旧订阅
const ref = useRef(null)
返回可变的 ref 对象,可挂 DOM 节点,也可存跨渲染不丢的值
const memoized = useMemo(() => compute(a, b), [a, b])
依赖不变时复用上次计算结果,避免昂贵计算重复执行

Hooks 进阶 6

const [state, dispatch] = useReducer(reducer, initialState)
复杂状态管理
const value = useContext(MyContext)
跨层级数据共享
useLayoutEffect(() => { measure() }, [deps])
同步执行 DOM 测量
useImperativeHandle(ref, () => ({ focus: () => {} }))
暴露子组件方法给父组件
const id = useId()
生成唯一 ID(SSR 安全)
const [isPending, startTransition] = useTransition()
非阻塞更新(React 18)

组件模式 6

function App({ name, children }) { return <div>{children}</div> }
props 与 children
const Button = forwardRef((props, ref) => <button ref={ref} />)
forwardRef 转发 ref
function withAuth(Component) { return (props) => <Component {...props} /> }
高阶组件(HOC)
function List({ render }) { return items.map(render) }
render props 模式
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")
创建 Context
<ThemeContext.Provider value="dark">
提供 Context 值
const value = useContext(ThemeContext)
消费 Context

性能优化 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>)}
列表渲染必须带 key
class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} }
错误边界捕获子组件错误
ReactDOM.createPortal(<Modal />, document.body)
Portal 渲染到组件树外
<Comp {...defaultProps} {...props} />
合并默认 props 与传入 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. 1确认某段逻辑到底跑了几次

    useEffect(() => { console.log("effect run") }, [])

    在 effect 里打点日志;开发环境出现两次通常是 StrictMode 的挂载-卸载-再挂载,只要清理逻辑写对就不影响生产。

  2. 2定位是哪一次渲染创建了过期闭包

    const handler = useCallback(() => {}, [deps])

    回调里引用的每个外部变量都应出现在 deps 中;用 useCallback 固定引用,再配合 React.memo 让子组件跳过重渲染。

  3. 3把重渲染限制在必要范围内

    const MemoComp = React.memo(MyComponent)

    先用 React DevTools Profiler 确认哪些组件在无谓重渲染,再对症加 React.memo / useMemo / useCallback,而不是到处包。

  4. 4按需渲染与列表渲染的最小正确写法

    {isLoading ? <Loading /> : <Content />}

    条件渲染用三元或短路表达式;列表渲染必须带稳定 key,二者都要出现在本表的渲染分组里。

  5. 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日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们