Agent Developer Roadmap
前端框架

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 风格,如 classNamehtmlFor,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/DOMref + 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 SnapshotQueueing 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

概念发生阶段可以做什么不应做什么
组件 RenderRender读取输入并返回 JSX写 DOM、发请求、改外部变量
事件处理器浏览器事件执行用户命令、排队状态依赖 setter 后立即读到新状态
useMemoRender缓存昂贵纯计算承载正确性或副作用
useEffectCommit 后同步网络、订阅、DOM 小部件复制可在 Render 派生的状态
useLayoutEffectCommit 后、绘制前必须同步测量并调整布局普通数据加载与非视觉副作用

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 包含 qcompare,刷新后恢复状态。搜索只有八项数据,因此不使用 useTransition 或 Worker;把简单同步过滤标成低优先级只会增加 pending 状态而无可测收益。

Transition 用于把某些状态更新标为非阻塞工作。它可以被输入等紧急更新打断并重启,但不能控制文本输入本身。React 19.2 的 useTransition 允许 Action 包含异步工作,不过 await 后的某些状态更新仍需再次包进 startTransition;自建异步 Action 还必须处理响应乱序。useTransition

Effect 生命周期与外部系统同步

React 示例在一个组件内保留查询、选择、详情和加载状态。八项固定数据由 useMemo 派生搜索结果;选择名称直接从条目和 id 派生,没有第二份同步状态。

运行效果React 知识目录运行效果
新窗口打开

正在加载示例……

数据加载 Effect 创建 AbortController,cleanup 取消未完成 Fetch。URL Effect 只同步外部 History 系统。dialog.showModal() 由另一个 Effect 协调原生控件;关闭事件恢复触发按钮焦点。这些都是真正的外部系统,符合 Effect 的用途。官方文档明确指出,没有外部系统时通常不需要 Effect。useEffect

开发入口使用 StrictMode。它在开发环境额外执行一次 Effect setup/cleanup,用来暴露清理不对称;这不是生产环境“双请求”的承诺,也不应通过布尔 ref 隐藏问题。请求应可取消,订阅应可解绑,DOM 小部件应可恢复。

示例状态保存位置原因
技术目录useState异步输入,加载后替换一次
查询词useState + URL受控输入且需要分享
搜索结果useMemo 派生不建立双真值
选择 iduseState + 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;录制后确认昂贵纯计算,再使用 useMemomemouseCallback 是性能工具,不是修复不纯组件或错误依赖的语义工具。

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 18createRoot 启用并发基础、自动批处理、Transition 与流式 SSRRender 可重启;旧测试对同步提交的假设失效Strict Mode 清理、焦点、SSR 水合、测试 act
React 19/19.2Actions、表单、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、useActionStateuseOptimisticuse、表单 Action 与静态预渲染 API。19.2 又加入 Activity、useEffectEvent、Performance Tracks 和 SSR 改进。React v19React 19.2

React 当前文档标记最新系列为 19.2,并保留 18、17 等归档站点。Canary 通道用于框架提前集成接近完成的能力,不等同于应用默认稳定版本。React VersionsVersioning Policy

Server Components 在独立于客户端应用或 SSR 服务器的环境中提前渲染组件,并通过框架集成构建、传输和缓存。React 核心提供模型,但生产路由和部署需要框架;不要在无完整协议实现时手写 RSC bundler。Server Components

未来采用顺序:先升级安全 patch;再处理弃用与 Strict Mode 暴露的问题;然后评估 Compiler、Activity 或新的服务端能力是否减少已测复杂度。实验功能不进入关键路径,也不为了展示新 API 改写八项同步过滤。

React、原生平台与 Next.js 的职责差异

约束原生平台ReactNext.js
单页少量局部交互通常最小可用但有构建与运行时成本过重
大量复合组件手工协调成本上升组件和状态组合适合同样适合
文件路由与嵌套布局需自选 RouterReact 核心不提供内建 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 与安全公告

在 GitHub 上编辑此页

本页目录