React
React 的定位是「声明式 UI 库」:你描述"长什么样 + 数据怎么来",框架负责"何时以何种方式更新 DOM"。要驾驭 React,先建立渲染(render)心智模型,再学 Hooks 与并发特性,最后才谈状态管理。本文基于 React 19(2024-12-05 稳定,当前主线版本);示例为标准用法,可复制进
create-vite(react 模板)或官方模板直接运行。
一、渲染心智模型:先懂"什么时候重新渲染"
1.1 组件是一次函数调用
React 将 UI 描述为组件树。每次状态变化,React 会重新调用相关组件函数,拿新返回值与上次渲染结果做差异(reconcile),再把差异应用到 DOM:
function Counter() {
const [count, setCount] = useState(0)
// count 变化 → 本组件函数被重新调用 → 返回新的 <p> 描述 → React 只更新必要 DOM
return <p>你点了 {count} 次</p>
}关键推论:
- render 必须纯净:不要在 render 中做副作用(发请求、改全局、写 localStorage);
- 组件不是"模板":它每次都完整执行——
console.log出现在 render 里就会每次渲染都打(不优化时); - props 是快照:本次渲染的 props/state 在本次调用里恒定不变,事件回调闭包捕获的是"触发那一刻"的值。
1.2 什么时候会重新渲染
| 触发原因 | 说明 | 常见误解 |
|---|---|---|
| 自身 state 变化 | setState 后必然重渲染本组件 | 把"改 props 不行就改 state"当银弹 |
| 父组件渲染 | 默认向下传染:父 render → 所有子组件都 render(哪怕子组件没变化) | "我 memo 了子组件就不会"——条件见 §3 |
| context 值变化 | 订阅该 context 的组件重渲染 | 把整个 store 塞进一个 context |
| props 引用变化 | 每次父渲染都新建对象/函数,memo 失效 | useCallback 依赖没写全 |
1.3 key 决定"身份"而非"位置"
列表 diff 靠 key 匹配新旧元素。索引当 key 在插入/删除/重排时会张冠李戴(状态错位、输入框串行):
// ❌ index 作 key:在头部插入一项,其后所有项都被当成"同一个组件换了内容"
{items.map((it, i) => <Row key={i} item={it} />)}
// ✅ 稳定唯一 id;没有就用内容哈希/库生成(crypto.randomUUID 仅在创建时生成一次)
{items.map((it) => <Row key={it.id} item={it} />)}1.4 派生状态:别复制 state,渲染时算
React 官方推荐"derive state during render":能从 props/state 算出来的,就不要另存一份要手动同步的 state:
// ❌ 反模式:用 effect 把 props 同步进 state(双数据源,时序坑)
const [list, setList] = useState(props.items)
useEffect(() => setList(props.items), [props.items])
// ✅ 渲染期直接派生;真正"只读一遍"的历史值才需要存(见 §3.5 的 key 换组件)
const shown = props.items.filter((it) => it.active)二、Hooks 的正确用法
2.1 规则与心智锚点
- 只在组件 / 自定义 Hook 顶层调用,不要在循环、条件里调(顺序 = 状态槽位);
useState的 setter 更新的是快照队列,连续多次setCount(count + 1)不会累加——用函数式更新setCount(c => c + 1);- 对象/数组状态要替换而非修改:
setUser({...user, name: 'x'}),否则引用不变不触发渲染。
2.2 useEffect:同步外部系统,不是"数据流"
大多数 state 联动都应写成"渲染时派生",effect 只留给与外部系统同步(订阅、DOM 操作、请求——请求本身最好交给数据请求库):
useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') close() }
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey) // 清理必须成对
}, [close])- 依赖数组要诚实:漏依赖是 bug(闭包拿旧值),把"我不希望它重跑"写进依赖是自欺——正确做法是重组依赖(用 ref 保存"只用于回调的最新值",或把不稳定函数放进 effect 内定义);
- Strict Mode(开发环境)会双执行 effect:模拟"挂载→卸载→挂载",帮你暴露清理遗漏,不是 bug。
2.3 useMemo / useCallback:只挡"代价高或引用不稳定"的
const sorted = useMemo(() => heavySort(list), [list]) // 昂贵计算才用
const onClick = useCallback(() => doThing(id), [id]) // 传给 memo 子组件才用心智锚点:
memo只是"props 引用没变就跳过子组件 render"。而父组件每次 render 都会新建函数/对象——所以memo必须配合useCallback/useMemo稳定引用才有效。纯计算不配 memo、useMemo 依赖每次都在变都是无效优化。React 19 起编译优化方向见 §6,别手工到处套。
2.4 useRef:跨渲染的可变值 + DOM 引用
const idRef = useRef(0) // 可变但不变不触发渲染
const inputRef = useRef(null) // DOM 引用
// React 19:函数组件可直接接收 ref prop(不再必须 forwardRef)
function Field({ ref, ...props }) { return <input ref={ref} {...props} /> }2.5 自定义 Hook 的组合纪律
把逻辑抽成 useXxx:内部用现有 hooks 组合,返回"值 + 操作"。命名以 use 开头(eslint 检查规则依赖它)。注意同一 hook 调用一次只服务一个逻辑——不要在 setter 里做两件事的编排,编排放调用方。
三、性能:把"重渲染"管住
3.1 自上而下的默认 vs 精确跳过
React 默认"宁可多渲染也不错漏"。优化顺序:
- 先确认慢在哪(Profiler / React DevTools Highlight updates)——多数场景根本不用动;
- 结构优化:把常变的 state 下沉到最小子树(状态放哪,影响范围就在哪);
memo+ 稳定引用:隔离低频子树;- 极端场景(列表项成百上千且频繁更新)才考虑列表虚拟化或自行拆分。
3.2 两个"延迟/降级"并发 API
| API | 效果 | 典型场景 |
|---|---|---|
useTransition(返回 isPending) | 把状态更新标为"可中断的低优先级" | 切换大表格/筛选,UI 立刻响应、完成前保留旧内容 |
useDeferredValue | 生成"延后版本"的值去渲染慢部分 | 搜索框:输入即时高亮,结果列表用延后值渲染 |
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query) // 让出主线程渲染旧列表,直到不忙并发渲染是 React 的"实现细节能力":渲染可中断、可丢弃低优先级结果。它不是把同步计算变快,而是让"慢更新"不再卡住"急更新"(输入框不丢字)。
3.3 Suspense 与数据获取
Suspense 让组件可以"挂起(suspend)"并显示 fallback,直到资源就绪:
<Suspense fallback={<Spinner />}>
<ProfilePanel userId={id} /> {/* 内部通过 use()/数据库读取并挂起 */}
</Suspense>配合服务端框架(Next.js App Router 等)可做流式 SSR:先发壳与关键内容,慢的数据以 HTML 流分块到达,客户端再补水合,LCP 显著改善。
3.4 库作者:useSyncExternalStore
从外部 store 读状态且要并发安全(避免 tearing),交给 useSyncExternalStore:
const count = useSyncExternalStore(store.subscribe, store.getSnapshot)应用层一般不需要直接写它——它是状态库(Zustand 等)的底层实现。
四、Actions:表单与异步更新的第一公民(React 19)
React 19 把"提交动作"标准化为 Actions——传给 transition / <form action> 的异步函数,React 自动跟踪 pending、自动重置受控表单、把错误抛给最近错误边界:
function useSubmit() {
const [state, formAction, isPending] = useActionState(
async (prev, formData) => {
const res = await api.create({ title: formData.get('title') })
return { ok: true, id: res.id } // 返回值成为下一次 prev / state
},
{ ok: false },
)
return { state, formAction, isPending }
}
function CreateForm() {
const { state, formAction, isPending } = useSubmit()
return (
<form action={formAction}>
<input name="title" required />
<button disabled={isPending}>{isPending ? '提交中…' : '创建'}</button>
{state.ok && <p>已创建 #{state.id}</p>}
</form>
)
}配套:
useOptimistic:提交期间先渲染乐观值,成功后回落到服务端真实值;useFormStatus:表单后代组件读取父 form 的 pending(用于拆出按钮组件);<form>action 是纯客户端能力;要服务端执行交给 Next.js 等的 Server Actions。
五、状态管理选型:先本地,再全局
5.1 演进阶梯(按需上,别一步到位)
| 层级 | 手段 | 何时用 |
|---|---|---|
| 1 | 组件内 useState/useReducer + 渲染期派生 | 状态只影响自身或子树 |
| 2 | 状态提升 + props 下传 | 少量兄弟/父子共享 |
| 3 | useContext + useReducer | 中等规模、读多写少(拆分 context:值与 dispatch 分开,避免全树重渲) |
| 4 | 外部库(Zustand / Redux Toolkit / Jotai) | 跨模块大范围共享、需要 devtools/持久化/时序工具 |
| 附加维度 | TanStack Query / SWR(服务端缓存) | 与后端交互的数据:请求去重、缓存、失效重取——与客户端状态正交 |
经验法则:多数 CRUD 界面里,真正需要"全局客户端状态"的往往只有"当前用户/主题/布局"。服务端数据请默认交给数据请求库,别用全局 store 当请求缓存(失效与并发难管)。
5.2 需要 devtools 与中间件:Redux Toolkit
项目已有成熟 RTK 基建、跨团队规范、需要时间旅行调试时选它。样板是它最大的争论点,createSlice 已大幅压缩:
// features/cartSlice.js(RTK 标准写法)
import { createSlice } from '@reduxjs/toolkit'
export const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
add(state, action) { state.items.push(action.payload) }, // Immer 允许"写"语法
clear() { return { items: [] } },
},
})5.3 轻量跨组件:Zustand(v5)
store 是普通 hook,无 Provider 包裹;基于 useSyncExternalStore,选择器化读取避免多余重渲染:
import { create } from 'zustand'
export const useCart = create((set) => ({
count: 0,
add: () => set((s) => ({ count: s.count + 1 })),
}))
function CartBadge() {
const count = useCart((s) => s.count) // 只订阅 count:变化才重渲染
return <span>{count}</span>
}坑:Zustand 不加选择器直接
useCart()会订阅整个 store(任何字段变都重渲染);选择器返回新对象/数组(如useCart((s) => s.items.filter(...)))时又可能死循环——用 v5 的useShallow或在 store 内维护派生字段。
六、React 19 特性速览与 19 之后的方向
| 特性 | 一句话 |
|---|---|
Actions / useActionState / useOptimistic / useFormStatus | 异步更新内建 pending、乐观更新与错误处理 |
<form action> | 表单直接提交给函数(客户端动作) |
ref 作为 prop | 函数组件免 forwardRef |
use() | 在条件/循环里读 Context 或 Promise(配合 Suspense) |
| 文档元数据提升 | 组件里写的 <title>/<meta> 自动上移到 <head> |
<Suspense> 为默认边界 | 资源加载失败就近回退 |
| React Compiler | 自动记忆化(memo/useMemo/useCallback 可逐步删除);以独立编译层启用,配合 ESLint 检查 |
未来方向一句话:Actions + Compiler 正在把"手动优化渲染"变成框架内建能力——状态管理、渲染优化的心智会继续往"默认快 + 少配置"收敛,但渲染模型与 Hooks 规则不会变。
七、常见坑速查
- 依赖数组漏写/乱写:闭包拿旧 state、effect 意外重跑——用 lint(
exhaustive-deps)兜底,写不进去的依赖用 ref 方案而非注释; - 派生状态用 effect 同步:双数据源闪屏/竞态——渲染期派生(§1.4);
- memo 无效:父组件每次新建 props 引用、或依赖没写全——配合
useCallback,先量再优; - state 更新不生效:直接改了对象/数组没有替换引用(§2.1-3);
- 事件回调闭包过期:快速连续交互读到旧值——用函数式更新或
useEffectEvent(实验)/ref 保持最新; - 把请求写在 useEffect + 手工取消防抖:重复挂载双发、竞态、无缓存——直接上 TanStack Query/SWR,把"取数"从手写逻辑里拿掉;
- setState 在 render 中同步调用:除非是官方支持的"渲染期重置派生状态"模式(比较 props 变化后
setState并立即 return),否则死循环; - 过度优化:万物 memo/useCallback,热路径之外净增心智负担——先 Profiler 定位,再动手;
- 把"全局 store"当唯一数据层:服务端数据进 store 导致缓存/失效自己造轮子——按 §5.1 阶梯分层。
状态与参考
- 状态:已收录(2026-09-02,由前端领域规划清单「React|Hooks 心智模型、并发特性、状态管理选型」转正式)。
- 版本:基于 React 19(2024-12-05 稳定;19.1/19.2 为补丁与特性小步,个别 API 差异见 官方 Releases)。文中 JSX/示例为标准写法,可复制进
create-vite(react 模板)直接运行;Zustand 示例基于 v5,RTK 为现代createSlice写法。 - 参考:React 官方文档(Learn / API reference)、React 19 发布说明、React Compiler、Zustand、Redux Toolkit、TanStack Query。
- 阅读联动:与 Vue/Angular/Svelte 的定位差异对照前端领域概览与 Vue 3/Angular/Svelte/Astro;类型配合见 TypeScript 工程实践。
下一步
- [ ] 镜像 React 官方文档的分篇页面(类似 Vue 3 栏目),把 Hooks、并发、Actions 展开为独立子页
- [ ] 用 TanStack Query + Zod 写一个"服务端状态管理"完整迷你示例(含失效与乐观更新)
- [ ] 对照 Vue 与 Svelte 写一篇"信号三阵营(依赖收集/脏检查/信号)"的渲染机制对比
写作规范请参阅领域概览。