React:用状态快照协调可中断渲染
最后核验:2026-06-30|适用版本:React 17–19.2(示例锁定 19.2.7)|前置知识:JavaScript、TypeScript、Browser / Web APIs
React 的适用约束与固定成本
React 的核心价值在于把界面描述成“给定 props、state 与 context 后的渲染结果”,再由运行时协调更新、提交 DOM 和执行 Effects;JSX 只是其中一种表达方式。理解这个模型,才能解释事件处理器为什么读到旧状态、开发模式为何重复执行 Effect、Transition 为什么可以被更紧急的输入打断,以及服务端组件为什么不等于传统 SSR。
React 适合组件数量多、状态关系复杂、团队需要统一组合规则或生态集成的应用。以下情况不应先选 React:静态内容页只有少量增强;原生表单和 CSS 已能完成交互;团队真正需要的是文件路由、服务端数据加载和缓存,却只安装了客户端 React。React 官方已经弃用 Create React App 作为新项目起点,建议使用框架或 Vite 等构建工具。Sunsetting Create React App
这里把 React 与 Next.js 分开讨论:React 解释组件、状态与调度;Next.js 解释路由、Server Components、缓存和部署契约。把二者混成一个技术,会把框架行为误归因于 React 核心。
JSX、组件与首次挂载
JSX 是 JavaScript 的语法扩展,不是浏览器原生模板。构建器把 <Button label="保存" /> 转换为 JSX runtime 调用,产生描述 element type、props、key 等的不可变 React element;它不是 DOM node。React 再根据 element tree 创建/复用 Fiber,最终在 Commit 阶段创建真实 DOM。
type ButtonProps = {
label: string;
pressed?: boolean;
onPress(): void;
};
function ToggleButton({ label, pressed = false, onPress }: ButtonProps) {
return (
<button type="button" aria-pressed={pressed} onClick={onPress}>
{label}
</button>
);
}组件函数接收 props 并返回 React node。props 是父组件给本次 Render 的只读快照;不要修改 props 对象。组件名必须大写,否则 JSX 把它当宿主标签。返回 null 表示不输出节点,但组件仍参与 Render 和 Hook 生命周期。
JSX 表达式默认转义文本;属性名遵循 DOM property 风格,如 className、htmlFor,data/aria 属性保留连字符。事件 prop 接收函数,写 onClick={save()} 会在 Render 立即调用;应写 onClick={save} 或包装参数。
首次挂载流程:createRoot(container) 建立 root;root.render(element) 调度 Render;组件函数生成 element;reconciler 建立 Fiber/宿主节点工作;Commit 插入 DOM、设置 ref、运行 layout effects;浏览器绘制后运行 passive effects。开发 StrictMode 可能额外调用纯函数和 effect setup/cleanup,生产挂载不等于开发次数。
state、ref、context 与 reducer 的职责
useState 保存影响渲染的局部状态;useRef 保存跨 Render 的可变值但修改不触发 Render,适合 DOM ref、timer id、最新外部对象;Context 向子树提供跨层依赖,每次 provider value 身份变化会通知消费者;useReducer 把多事件状态转换集中为纯函数。
| 数据 | 放置位置 | 反例 |
|---|---|---|
| 能由 props/state 纯计算 | Render 中直接派生 | 再建 state + Effect 同步 |
| 输入框、展开、选择 | 最近共同拥有者 state | 默认全部放全局 store |
| timer/AbortController/DOM | ref + effect cleanup | 放 state 导致无意义 Render |
| 主题/认证会话接口 | 稳定 Context 边界 | 每次 Render 新建巨大 value 对象 |
| 多事件有限状态 | reducer/判别事件 | 多个 setter 相互修补非法组合 |
Hook 必须在组件或自定义 Hook 顶层以稳定顺序调用。不能在条件、循环、普通回调中调用,因为 Fiber 按调用顺序关联 Hook state;这不是代码风格偏好,而是运行时寻址契约。
状态快照、Render 与 Commit
一次界面更新分为 Trigger、Render、Commit。初次 root.render() 或状态更新触发工作;Render 阶段调用组件计算下一棵树,必须保持纯;Commit 阶段把必要差异写入 DOM。浏览器随后才进行布局和绘制。React 官方将这三步作为理解更新的基础。Render and Commit
Render 可以暂停、重启或放弃,Commit 才改变用户可见 DOM。因此不能在组件函数里发请求、修改 DOM 或写存储;这些副作用可能执行多次却从未提交。事件处理器适合由用户动作直接引起的命令,Effect 只用于把已提交界面同步到 React 之外的系统。
状态是某次 Render 的状态快照。调用 setter 只是请求下一次 Render,不会改变当前事件处理器闭包里的值。多个更新会批处理;同一状态依赖上一结果时使用函数式更新,让 React 按队列依次计算。State as a Snapshot、Queueing State Updates
setSelected((current) => current.includes(id)
? current.filter((item) => item !== id)
: current.length < 4 ? [...current, id] : current);React 根据组件在 Render Tree 中的位置保存状态,而不是根据 JSX 文本或函数调用次数。位置或 key 改变会重置状态;稳定但错误的数组索引 key 会把输入、焦点或局部状态错配给另一条数据。Preserving and Resetting State
| 概念 | 发生阶段 | 可以做什么 | 不应做什么 |
|---|---|---|---|
| 组件 Render | Render | 读取输入并返回 JSX | 写 DOM、发请求、改外部变量 |
| 事件处理器 | 浏览器事件 | 执行用户命令、排队状态 | 依赖 setter 后立即读到新状态 |
useMemo | Render | 缓存昂贵纯计算 | 承载正确性或副作用 |
useEffect | Commit 后 | 同步网络、订阅、DOM 小部件 | 复制可在 Render 派生的状态 |
useLayoutEffect | Commit 后、绘制前 | 必须同步测量并调整布局 | 普通数据加载与非视觉副作用 |
Fiber、Reconciliation 与列表 Diff
Fiber 是 React 对组件/宿主节点工作单元的内部表示,保存 type、key、props/state、父子兄弟关系、待提交 effect flags 与调度信息。Render 阶段构建 work-in-progress tree 并与 current tree 对照;并发能力允许工作按优先级示例、暂停或放弃。Fiber 字段不是公共 API,理解它是为了区分可中断 Render 与一次性 Commit,不应让业务读取内部属性。
Reconciliation(协调)依赖启发式:不同 element type 通常替换整棵对应子树;相同 type + key 尝试复用实例/Fiber/DOM 并更新 props;同层列表用 key 匹配旧新子项。React 不做任意树的最优编辑距离,开发者用稳定 key 提供身份。
function EntryList({ entries }: { entries: Entry[] }) {
return (
<ul>
{entries.map((entry) => (
<li key={entry.id}>
<label>
{entry.name}
<input defaultValue={entry.note} />
</label>
</li>
))}
</ul>
);
}若改用数组 index,插入第一项后旧 Fiber/输入 DOM 会按位置复用给不同业务条目,未受控输入、焦点、动画和子组件 state 可能错位。key 只需在当前 siblings 中唯一且稳定,不会作为 prop 传给组件;需要 id 时另传 id。
| 变化 | 协调结果 | 状态影响 |
|---|---|---|
| 同位置、同 type、同 key | 复用并比较 props/children | 保留 state |
| 同位置 type 改变 | 卸载旧子树、挂载新子树 | 重置 state/effects |
| key 改变 | 视为新身份 | 主动重置 state |
| 列表移动且 key 稳定 | 匹配旧 Fiber,Commit 移动 DOM | 保留条目 state |
| Fragment 缺 key 的动态集合 | 退回位置匹配/警告 | 可能错配 |
Commit 大致包含 mutation、layout、passive effect 等工作:先执行必要 DOM 变更与旧 layout cleanup,再设置 ref/运行新 layout effect,绘制后调度 passive effect。不要依赖未文档化的兄弟 effect 精确顺序;需要顺序的业务操作放同一边界显式协调。
批处理、Transition 与更新优先级
下面的组件展示状态快照、纯派生和 Effect 边界。环境是 React 19.2、TypeScript 6 与 Vite 8。
import { useEffect, useMemo, useState } from 'react';
type Entry = { id: string; name: string; tags: string[] };
export function Catalog({ entries }: { entries: Entry[] }) {
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<string[]>([]);
const filtered = useMemo(() => {
const term = query.trim().toLowerCase();
return entries.filter(({ name, tags }) =>
[name, ...tags].some((value) => value.toLowerCase().includes(term)));
}, [entries, query]);
useEffect(() => {
const params = new URLSearchParams();
if (query) params.set('q', query);
if (selected.length) params.set('compare', selected.join(','));
history.replaceState(null, '', `?${params}`);
}, [query, selected]);
function toggle(id: string) {
setSelected((current) => current.includes(id)
? current.filter((item) => item !== id)
: current.length < 4 ? [...current, id] : current);
}
return <>
<label htmlFor="q">搜索</label>
<input id="q" type="search" value={query}
onChange={(event) => setQuery(event.currentTarget.value)} />
<ul>{filtered.map((entry) => <li key={entry.id}>
{entry.name}
<button type="button" aria-pressed={selected.includes(entry.id)}
onClick={() => toggle(entry.id)}>对比</button>
</li>)}</ul>
</>;
}从仓库根目录运行完整示例:
pnpm --filter @roadmap/react-slice test
pnpm --filter @roadmap/react-slice preview --port 4201输入:访问 http://127.0.0.1:4201/,搜索 server,选择四项后尝试第五项,再刷新页面。
预期结果:5 项测试与生产构建通过;筛选结果随输入更新,选择不超过四项,URL 包含 q 与 compare,刷新后恢复状态。搜索只有八项数据,因此不使用 useTransition 或 Worker;把简单同步过滤标成低优先级只会增加 pending 状态而无可测收益。
Transition 用于把某些状态更新标为非阻塞工作。它可以被输入等紧急更新打断并重启,但不能控制文本输入本身。React 19.2 的 useTransition 允许 Action 包含异步工作,不过 await 后的某些状态更新仍需再次包进 startTransition;自建异步 Action 还必须处理响应乱序。useTransition
Effect 生命周期与外部系统同步
React 示例在一个组件内保留查询、选择、详情和加载状态。八项固定数据由 useMemo 派生搜索结果;选择名称直接从条目和 id 派生,没有第二份同步状态。
数据加载 Effect 创建 AbortController,cleanup 取消未完成 Fetch。URL Effect 只同步外部 History 系统。dialog.showModal() 由另一个 Effect 协调原生控件;关闭事件恢复触发按钮焦点。这些都是真正的外部系统,符合 Effect 的用途。官方文档明确指出,没有外部系统时通常不需要 Effect。useEffect
开发入口使用 StrictMode。它在开发环境额外执行一次 Effect setup/cleanup,用来暴露清理不对称;这不是生产环境“双请求”的承诺,也不应通过布尔 ref 隐藏问题。请求应可取消,订阅应可解绑,DOM 小部件应可恢复。
| 示例状态 | 保存位置 | 原因 |
|---|---|---|
| 技术目录 | useState | 异步输入,加载后替换一次 |
| 查询词 | useState + URL | 受控输入且需要分享 |
| 搜索结果 | useMemo 派生 | 不建立双真值 |
| 选择 id | useState + URL | 小型可序列化用户状态 |
| 已选条目 | Render 中派生 | 条目或 id 改变时自然一致 |
| 当前详情 | useState | 短暂界面状态,无需持久化 |
这里不引入 Redux、Zustand、React Router 或 Query 库。单页、一次只读加载和四项本地选择没有对应约束;出现跨路由服务端缓存、乐观写入或多团队状态所有权后,再按实际问题选工具。
Effect 依赖、闭包与易错 API
Effect dependency list 描述 Effect setup 读取的 reactive values,不是手工选择“何时运行”的开关。props、state、组件内函数/对象都会随 Render 形成新快照;遗漏依赖会让闭包读取旧值,随意加入每次新建对象又会让 Effect 重连。修复顺序是移除不必要 Effect、把对象/函数移入 Effect 或组件外、使用函数式 state updater,最后才 memoize。
useEffect(() => {
const controller = new AbortController();
void loadEntries(query, controller.signal)
.then(setEntries)
.catch((error) => {
if (!controller.signal.aborted) setLoadError(String(error));
});
return () => controller.abort();
}, [query]);useEffectEvent 适合“由 Effect 触发但需要读取最新非响应值”的事件逻辑,不应拿来隐藏真实依赖。useLayoutEffect 阻塞绘制,只用于必须测量/修正布局的路径;服务端渲染无法执行浏览器 layout effect。useImperativeHandle 只暴露父级确实需要的最小命令,不把整个子组件内部状态做成 ref API。
受控 input 的 value 由 React state 驱动,需要同步 onChange;从 undefined 切到 string 会产生受控/非受控切换警告。文件 input 必须非受控。表单提交优先保留 <form> 原生语义,React 19 form Action 再增强 pending、乐观状态和错误处理;客户端隐藏按钮不构成授权。
常见 Hook 易错点:
useMemo/useCallback的结果是性能缓存,不保证永不丢失,不能承载唯一对象身份或副作用。- ref.current 在 Render 中读写(惰性确定初始化除外)破坏纯性,并发重启时可能泄漏。
- Context provider 的 inline object 每次创建新身份,使所有消费者更新;按领域拆 context 或 memoize 已测热点。
useId生成水合稳定的可访问性 id,不用于列表 key,也不持久化为业务 id。flushSync强制同步提交,可能刷新 pending work/Suspense,只在第三方宿主必须立即读 DOM 时使用。
SSR、水合与 Server Components 边界
传统 SSR 在服务器把同一客户端组件树渲染为 HTML,浏览器先显示 HTML,再由 hydrateRoot 关联事件和 Fiber。水合要求服务端 HTML 与客户端首次 Render 的可观察结果一致;时间、随机数、locale、浏览器 API、无效 HTML 解析纠错都会造成 mismatch。
suppressHydrationWarning 只抑制一个明确不可避免的文本/属性差异,不递归修复结构,也不是通用消音器。正确修复是让初始输入确定一致、在 effect 后读取浏览器状态,或把真正仅客户端区域明确隔离。
Server Components 在服务器环境执行并通过 RSC payload 描述树、模块引用和数据,不把组件代码发到客户端;Client Component 边界接收可序列化 props并在浏览器交互。RSC 不等于 SSR:框架可把 RSC 结果进一步 SSR 为 HTML,也可用于客户端导航。路由、bundler、缓存和安全协议由 Next.js 等框架集成,React 应用不自行拼协议。
Profiler、Performance Tracks 与安全边界
React DevTools 用于检查组件树、props、state 和 Profiler;浏览器 Performance 仍是主线程、布局和网络的事实来源。React 19.2 增加 Performance Tracks,可在 Chrome 性能录制中查看 Scheduler 优先级和组件工作,但不能用 Render 次数直接推断用户可见性能。React 19.2
性能顺序是先减少工作,再缩小更新边界,最后才缓存:删除重复状态和无意义 Effect;把状态放到真正拥有它的最小共同父节点;稳定 key;录制后确认昂贵纯计算,再使用 useMemo。memo 与 useCallback 是性能工具,不是修复不纯组件或错误依赖的语义工具。
React Compiler 1.0 已稳定,可在构建期自动进行记忆化;它不消除状态建模、Effect 清理或测量需求。采用前应启用官方 lint 规则并增量验证构建与第三方库兼容性,不要同时保留大量手写 memo 而从不复测收益。React Compiler v1.0
安全上,JSX 默认转义文本,但 dangerouslySetInnerHTML 会绕过该边界。不可信 HTML 必须在可信服务端净化并限制来源。React Server Components 在 2025 年出现严重反序列化漏洞;受影响框架即使没有显式 Server Function 也可能暴露风险,必须使用官方修复版本。React 19.2 线最低修复为 19.2.1,本示例锁定 19.2.7。React Server Components 安全公告
常见误区:把 setter 当同步赋值;用 Effect 计算筛选结果;缺依赖后关闭 lint;在 Render 中修改模块变量;给每个回调套 useCallback;用数组索引作为可重排列表 key;把客户端隐藏当权限控制;看到水合警告后使用 suppressHydrationWarning 掩盖不确定输出。
重大重构、严重漏洞与 React 17 到 19.2 迁移
| 节点 | 重大变化 | 严重回归或兼容边界 | 必须验证 |
|---|---|---|---|
| React 17 | 事件委托从 document 移到 root,支持多版本渐进升级 | 依赖 document 级合成事件顺序的集成会变化 | 嵌套 root、原生监听器、冒泡和 stopPropagation |
| React 18 | createRoot 启用并发基础、自动批处理、Transition 与流式 SSR | Render 可重启;旧测试对同步提交的假设失效 | Strict Mode 清理、焦点、SSR 水合、测试 act |
| React 19/19.2 | Actions、表单、use、RSC 稳定模型、Activity、Effect Events、部分预渲染 | RSC 打包接口不遵循普通 minor 稳定性;SSR Suspense reveal 行为调整 | 框架锁版本、流式边界、Action 幂等、Compiler lint |
| Compiler 1.0 | 构建期自动记忆化,减少手写 memo | 不纯 Render 与违反 Rules of React 的代码不能靠编译器修复 | 增量启用、lint、产物和真实交互录制 |
2025-12 的 React Server Components 漏洞不是一般升级提示:CVE-2025-55182 可通过恶意 Server Function/RSC 载荷造成未认证远程代码执行,CVSS 10.0;19.0、19.1.0/19.1.1、19.2.0 的 react-server-dom-* 受影响,首轮修复为 19.0.1、19.1.2、19.2.1。随后公告又加入源代码泄露与拒绝服务问题,并在 2026-01 更新 CVE-2026-23864。处理方式是按官方公告升级当前 release line 的最新 patch,核对框架实际解析到的 react-server-dom-*,重新构建和部署;WAF 或托管商临时缓解不能替代升级。React RSC 安全公告
安全回归检查应覆盖:未登录请求是否能到达 Server Function、Action 是否重新认证授权、错误是否泄露源码/堆栈、超大或循环载荷是否耗尽进程、部署中是否残留旧构建产物。纯客户端 React 且没有 RSC bundler 集成不受该 RSC 漏洞影响,但仍要审查 dangerouslySetInnerHTML、URL 和第三方脚本边界。
React 17 主要提供渐进升级和新的事件委托基础;React 18 引入并发渲染基础、自动批处理、Transitions 与新的 root API;React 19 加入 Actions、useActionState、useOptimistic、use、表单 Action 与静态预渲染 API。19.2 又加入 Activity、useEffectEvent、Performance Tracks 和 SSR 改进。React v19、React 19.2
React 当前文档标记最新系列为 19.2,并保留 18、17 等归档站点。Canary 通道用于框架提前集成接近完成的能力,不等同于应用默认稳定版本。React Versions、Versioning Policy
Server Components 在独立于客户端应用或 SSR 服务器的环境中提前渲染组件,并通过框架集成构建、传输和缓存。React 核心提供模型,但生产路由和部署需要框架;不要在无完整协议实现时手写 RSC bundler。Server Components
未来采用顺序:先升级安全 patch;再处理弃用与 Strict Mode 暴露的问题;然后评估 Compiler、Activity 或新的服务端能力是否减少已测复杂度。实验功能不进入关键路径,也不为了展示新 API 改写八项同步过滤。
React、原生平台与 Next.js 的职责差异
| 约束 | 原生平台 | React | Next.js |
|---|---|---|---|
| 单页少量局部交互 | 通常最小 | 可用但有构建与运行时成本 | 过重 |
| 大量复合组件 | 手工协调成本上升 | 组件和状态组合适合 | 同样适合 |
| 文件路由与嵌套布局 | 需自选 Router | React 核心不提供 | 内建 App Router |
| 服务端数据和流式渲染 | 需自建服务 | 只提供底层服务端 API | 提供集成边界 |
| Server Components | 不适用 | 定义组件模型 | 实现路由、打包和部署集成 |
| 内容优先、少量交互岛 | 原生/静态生成合适 | 可能发送过多客户端代码 | 可做但需控制边界 |
React 与 Vue、Angular、Svelte 的主要差异不在“能不能做列表”,而在更新单位和工程约束:React 通过组件 Render 与调度协调;Vue 使用 Proxy 依赖追踪与编译优化;Angular 提供 Signals、DI 和完整工程体系;Svelte 把更多响应式工作移到编译期。最终选择必须结合团队、路由、渲染和部署,而非只比较语法长度。
Render、Effect 与焦点验收练习
- 预测同一事件中三次
setCount(count + 1)的结果,再改成函数式更新验证队列。 - 在 React DevTools 中定位一次父组件更新导致的子树 Render,并判断是否真的需要优化。
- 删除搜索结果状态,改为 Render 中派生,确认功能不变且少一个 Effect。
- 在 Strict Mode 下为 Fetch 加入取消,确认 setup/cleanup 对称且无陈旧结果覆盖。
- 用四个稳定 id 与数组索引分别作为 key,重排后检查输入状态和焦点。
- 为大列表录制同步更新与 Transition,只有指标改善时保留 Transition。
- 检查生产依赖是否高于 RSC 安全公告中的修复版本。
- 用键盘完成搜索、详情和对比,并确认关闭对话框后焦点返回。
验收标准:能区分 Render 与 Commit,解释状态快照和批处理,知道 Effect 只同步外部系统,并能用测量决定 memo 或 Transition。若答案只剩 Hook 调用顺序,说明尚未建立 React 更新模型。
React 官方文档、Release 与安全公告
- React 19.2 文档:当前稳定文档与历史版本入口;核验于 2026-06-30。
- Render and Commit:Trigger、Render、Commit 基础模型;核验于 2026-06-30。
- State as a Snapshot:状态快照与事件处理器闭包;核验于 2026-06-30。
- Synchronizing with Effects:Effect 与外部系统边界;核验于 2026-06-30。
- React 19.2 Release:Activity、Effect Events、调试轨道与 SSR 更新;核验于 2026-06-30。
- React Compiler v1.0:稳定编译器与增量采用;核验于 2026-06-30。
- React Server Components Security:受影响版本与修复线;核验于 2026-06-30。