本文档系统梳理 React 17、React 18、React 19 三个关键版本分别新增了什么、为什么新增、解决了什么问题、底层原理是什么,以及在业务项目中如何使用。React 17 更像"升级过渡版本",React 18 是"并发渲染基础设施落地版本",React 19 则进一步面向"异步、表单、Server Components 和编译优化"的工程化体验。
目录
- [React 17 18 19 的版本定位](#React 17 18 19 的版本定位 "#1-react-17-18-19-%E7%9A%84%E7%89%88%E6%9C%AC%E5%AE%9A%E4%BD%8D")
- 总览:三个版本新增能力对比
- [React 17 新增了什么](#React 17 新增了什么 "#3-react-17-%E6%96%B0%E5%A2%9E%E4%BA%86%E4%BB%80%E4%B9%88")
- [React 17 解决了什么问题](#React 17 解决了什么问题 "#4-react-17-%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98")
- [React 17 核心原理](#React 17 核心原理 "#5-react-17-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86")
- [React 17 如何使用与升级](#React 17 如何使用与升级 "#6-react-17-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8D%87%E7%BA%A7")
- [React 18 新增了什么](#React 18 新增了什么 "#7-react-18-%E6%96%B0%E5%A2%9E%E4%BA%86%E4%BB%80%E4%B9%88")
- [React 18 解决了什么问题](#React 18 解决了什么问题 "#8-react-18-%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98")
- [React 18 核心原理](#React 18 核心原理 "#9-react-18-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86")
- [React 18 如何使用与升级](#React 18 如何使用与升级 "#10-react-18-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8D%87%E7%BA%A7")
- [React 19 新增了什么](#React 19 新增了什么 "#11-react-19-%E6%96%B0%E5%A2%9E%E4%BA%86%E4%BB%80%E4%B9%88")
- [React 19 解决了什么问题](#React 19 解决了什么问题 "#12-react-19-%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98")
- [React 19 核心原理](#React 19 核心原理 "#13-react-19-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86")
- [React 19 如何使用与升级](#React 19 如何使用与升级 "#14-react-19-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8D%87%E7%BA%A7")
- [Hooks 与 API 演进对比](#Hooks 与 API 演进对比 "#15-hooks-%E4%B8%8E-api-%E6%BC%94%E8%BF%9B%E5%AF%B9%E6%AF%94")
- 渲染模式演进:同步、并发、异步资源
- 表单与异步状态管理演进
- [服务端渲染与 Server Components 演进](#服务端渲染与 Server Components 演进 "#18-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%B8%B2%E6%9F%93%E4%B8%8E-server-components-%E6%BC%94%E8%BF%9B")
- [从 React 17 升级到 18 再到 19 的建议路径](#从 React 17 升级到 18 再到 19 的建议路径 "#19-%E4%BB%8E-react-17-%E5%8D%87%E7%BA%A7%E5%88%B0-18-%E5%86%8D%E5%88%B0-19-%E7%9A%84%E5%BB%BA%E8%AE%AE%E8%B7%AF%E5%BE%84")
- 常见问题与最佳实践
1. React 17 18 19 的版本定位
React 17、18、19 不是简单的"多几个 API",而是 React 渲染模型持续演进的三个阶段。
text
React 17:
目标不是新增大量功能,而是降低未来升级成本。
核心关键词:渐进升级、新事件系统、兼容性过渡。
React 18:
正式引入并发渲染能力,让 React 可以中断、恢复、调度不同优先级的更新。
核心关键词:createRoot、自动批处理、并发特性、Suspense SSR。
React 19:
把异步操作、表单提交、资源预加载、ref、Server Components 等能力进一步产品化。
核心关键词:Actions、useActionState、useOptimistic、use、ref as prop、React Compiler 生态。
1.1 一句话概括
| 版本 | 一句话定位 |
|---|---|
| React 17 | 为渐进升级和多版本共存铺路的兼容性版本 |
| React 18 | 并发渲染正式落地的基础设施版本 |
| React 19 | 面向异步交互、表单、服务端组件和编译优化的体验增强版本 |
1.2 为什么这些版本很关键
React 的演进方向从"组件化 UI 库"逐渐变成"应用渲染与交互运行时"。
text
早期 React:
关注如何把状态渲染成 UI。
React 17:
关注大型应用如何安全升级。
React 18:
关注复杂交互下如何保持页面响应。
React 19:
关注异步数据、表单提交、服务端渲染和编译优化如何融入日常开发。
2. 总览:三个版本新增能力对比
| 版本 | 主要新增/变化 | 解决的问题 | 使用关键词 |
|---|---|---|---|
| React 17 | 新 JSX Transform | 使用 JSX 不再必须显式引入 React | jsx: react-jsx |
| React 17 | 事件委托从 document 移到 root 容器 | 多 React 版本共存、渐进升级更安全 | 根容器事件绑定 |
| React 17 | 事件池移除 | 异步读取事件对象更自然 | 不再需要 event.persist() |
| React 17 | 副作用清理时机调整 | 行为更接近浏览器真实渲染 | useEffect cleanup 异步化 |
| React 18 | createRoot |
启用并发渲染基础能力 | ReactDOM.createRoot |
| React 18 | 自动批处理 | 减少无意义重复渲染 | Automatic Batching |
| React 18 | startTransition / useTransition |
区分紧急和非紧急更新 | Transition |
| React 18 | useDeferredValue |
延迟低优先级 UI 更新 | Deferred Value |
| React 18 | useId |
SSR/CSR 稳定 ID | Accessible ID |
| React 18 | useSyncExternalStore |
外部 Store 并发安全订阅 | Store subscription |
| React 18 | useInsertionEffect |
CSS-in-JS 插入样式时机稳定 | CSS injection |
| React 18 | Suspense SSR Streaming | 服务端流式渲染和选择性 Hydration | Streaming SSR |
| React 19 | Actions | 异步提交和状态更新更统一 | <form action> / transition action |
| React 19 | useActionState |
表单/异步 Action 状态管理 | pending/error/result |
| React 19 | useFormStatus |
表单提交状态下沉到子组件 | submit pending |
| React 19 | useOptimistic |
乐观更新标准化 | optimistic UI |
| React 19 | use |
组件内读取 Promise/Context | Suspense integration |
| React 19 | ref 作为普通 prop |
减少 forwardRef 心智负担 |
function Input({ ref }) |
| React 19 | Provider 简写 | Context 使用更简洁 | <ThemeContext value={...}> |
| React 19 | Document Metadata | 组件内声明 title/meta/link | <title> <meta> |
| React 19 | 资源预加载 API | 更精细控制资源加载 | preload preinit |
| React 19 | 错误处理改进 | 避免重复报错,错误回调更清晰 | onCaughtError |
3. React 17 新增了什么
React 17 官方定位比较特殊:没有面向业务开发者的大量新特性,重点是让 React 更容易渐进升级。
3.1 新 JSX Transform
React 17 支持新的 JSX 转换方式。以前写 JSX 时必须引入 React:
jsx
import React from 'react';
function App() {
return <h1>Hello</h1>;
}
因为旧 JSX 编译结果大致是:
js
React.createElement('h1', null, 'Hello');
新 JSX Transform 编译后不再依赖当前作用域中的 React 变量。
jsx
function App() {
return <h1>Hello</h1>;
}
编译结果近似为:
js
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello' });
}
解决的问题
text
1. 写 JSX 不再必须 import React
2. 减少无用导入
3. 为未来编译优化提供更明确的入口
4. 对初学者更友好
如何使用
Babel 配置:
json
{
"presets": [
["@babel/preset-react", { "runtime": "automatic" }]
]
}
TypeScript 配置:
json
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
Vite/CRA 新版本通常已经默认支持。
3.2 事件委托位置变化
React 16 及以前,React 会把大多数事件监听委托到 document 上。
text
React 16:
<button onClick={...} />
↓
事件冒泡到 document
↓
React 在 document 统一处理事件
React 17 开始,事件委托绑定到 React 根容器上。
text
React 17:
root.render(<App />)
↓
事件委托到 root DOM container
↓
React 在 root 范围内处理事件
示意:
html
<div id="root-a"></div>
<div id="root-b"></div>
React 17 会分别在 #root-a、#root-b 上建立事件系统,而不是统一挂到 document。
解决的问题
text
1. 多个 React 版本共存时事件冲突更少
2. 微前端场景下不同 React 应用边界更清晰
3. 渐进升级时可以局部升级某个 React 根节点
4. 外部代码在 document 上 stopPropagation 对 React 影响降低
典型场景
text
一个老系统使用 React 16,新增模块使用 React 17/18。
如果事件都挂在 document 上,不同版本事件系统可能互相干扰。
React 17 改为挂根容器后,不同 React root 更容易隔离。
3.3 移除 SyntheticEvent 事件池
React 旧版本为了性能复用事件对象,事件回调执行完后会清空事件对象。
旧写法中异步访问事件对象会出问题:
jsx
function handleChange(event) {
setTimeout(() => {
console.log(event.target.value); // 旧版本可能为 null
}, 100);
}
以前需要:
jsx
function handleChange(event) {
event.persist();
setTimeout(() => {
console.log(event.target.value);
}, 100);
}
React 17 移除了 Web 端事件池,不再需要 event.persist()。
jsx
function handleChange(event) {
setTimeout(() => {
console.log(event.target.value); // React 17 后可正常读取
}, 100);
}
解决的问题
text
1. 降低事件系统心智负担
2. 避免异步访问 event 的常见坑
3. 现代浏览器下事件池带来的性能收益已经不明显
3.4 useEffect 清理时机调整
React 17 对副作用清理时机做了调整,使其更接近浏览器真实渲染过程。
简化理解:
text
React 16:
某些 cleanup 可能在浏览器绘制前同步执行。
React 17:
useEffect cleanup 更倾向于在浏览器绘制后异步执行。
解决的问题
text
1. 避免阻塞浏览器绘制
2. 让 effect 行为更符合"非阻塞副作用"的定位
3. 为后续并发渲染模型做准备
3.5 返回一致的 undefined 错误
React 17 对组件返回 undefined 的错误提示更统一。
jsx
function App() {
return;
}
这种写法通常意味着开发者忘记返回 JSX。React 会给出更明确的提示。
4. React 17 解决了什么问题
React 17 最大价值不是功能,而是迁移能力。
4.1 解决大型项目难升级问题
大型系统常见情况:
text
1. 老项目依赖 React 16
2. 新业务想用新版本 React
3. 一次性全量升级风险太高
4. 不同业务模块发布节奏不同
React 17 通过事件系统改造,让一个页面中多个 React 版本共存更可靠。
text
页面中可以同时存在:
React 16 老模块
React 17 新模块
React 18 新模块
4.2 解决微前端兼容问题
在 qiankun、single-spa 等微前端场景中,主应用和子应用可能使用不同 React 版本。
React 17 的根容器事件委托让事件边界更加清晰,更适合微前端共存。
4.3 解决事件对象异步访问坑
事件池移除后,开发者不再需要关心 event.persist(),事件对象在异步回调中更符合直觉。
4.4 为 React 18 并发能力做准备
React 17 本身没有直接启用并发渲染,但它调整了事件系统、副作用时机等基础设施,为 React 18 的并发特性降低升级风险。
5. React 17 核心原理
5.1 新事件系统原理
React 的事件系统不是简单地给每个 DOM 节点绑定原生事件,而是使用事件委托。
text
真实 DOM 点击
↓
事件冒泡到 React root container
↓
React 捕获原生事件
↓
构造 SyntheticEvent
↓
根据 Fiber 树找到对应组件回调
↓
按捕获/冒泡顺序执行 onClick/onClickCapture
React 17 的关键变化是顶层监听位置:
text
React 16:document.addEventListener(...)
React 17:rootNode.addEventListener(...)
5.2 为什么移到 root 更好
假设页面有两个 React 应用:
html
<div id="legacy-root"></div>
<div id="new-root"></div>
如果事件都在 document 处理,那么老应用和新应用的事件系统都可能捕获同一个事件。移动到各自 root 后,事件处理范围更明确。
text
legacy-root 内的点击 → legacy React 处理
new-root 内的点击 → new React 处理
5.3 新 JSX Transform 原理
旧转换依赖:
text
JSX → React.createElement
新转换依赖:
text
JSX → react/jsx-runtime 中的 jsx/jsxs 函数
这让编译器拥有更直接的 JSX 运行时入口,也减少了业务代码中无意义的 React 导入。
6. React 17 如何使用与升级
6.1 安装
bash
npm install react@17 react-dom@17
6.2 保持旧渲染方式
React 17 仍然使用:
jsx
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
注意:createRoot 是 React 18 的能力,不属于 React 17。
6.3 升级检查清单
text
[ ] 检查是否依赖 document 级别事件冒泡行为
[ ] 检查是否有外部 DOM 代码调用 stopPropagation
[ ] 删除不再必要的 event.persist
[ ] 开启新 JSX Transform 时确认构建工具版本支持
[ ] 检查 React、ReactDOM、测试库版本是否匹配
[ ] 微前端项目验证主子应用事件是否正常
6.4 适合升级策略
text
如果当前项目是 React 16:
建议先升级 React 17,解决兼容性问题,再升级 React 18。
如果是大型老项目:
可以先局部将新模块升级到 React 17/18,避免一次性全量改造。
7. React 18 新增了什么
React 18 是非常关键的版本,它正式引入并发渲染相关能力,但默认不是所有更新都变成并发,而是通过新的 root API 和特定 API 渐进启用。
7.1 createRoot
React 18 推荐使用:
jsx
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
旧方式:
jsx
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
如果 React 18 仍使用旧 ReactDOM.render,应用会以 React 17 兼容模式运行,很多新能力无法完整启用。
解决的问题
text
1. 给并发渲染提供入口
2. 区分旧同步 root 和新 concurrent root
3. 支持新的调度和批处理行为
7.2 自动批处理 Automatic Batching
React 18 之前,React 只会在 React 事件回调中批处理多次 setState。
React 17:
jsx
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 事件中:一次渲染
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 17 中:可能两次渲染
}, 1000);
React 18 使用 createRoot 后,Promise、setTimeout、原生事件、网络回调中的状态更新也会自动批处理。
jsx
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18:一次渲染
}, 1000);
解决的问题
text
1. 减少不必要的重复渲染
2. 提升性能
3. 让不同异步来源的状态更新行为更一致
退出批处理
如果必须同步拿到 DOM 更新,可以使用 flushSync。
jsx
import { flushSync } from 'react-dom';
flushSync(() => {
setOpen(true);
});
console.log(document.querySelector('.modal'));
7.3 startTransition 与 useTransition
React 18 引入 Transition,用于区分紧急更新和非紧急更新。
典型问题
搜索框输入时,同时更新输入框值和列表过滤结果:
jsx
function SearchPage() {
const [keyword, setKeyword] = useState('');
const [list, setList] = useState([]);
function onChange(e) {
const value = e.target.value;
setKeyword(value);
setList(filterHugeList(value));
}
}
如果 filterHugeList 很重,输入会卡顿。
React 18 写法
jsx
import { useTransition, useState } from 'react';
function SearchPage() {
const [keyword, setKeyword] = useState('');
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition();
function onChange(e) {
const value = e.target.value;
setKeyword(value); // 紧急更新:输入框立即响应
startTransition(() => {
setList(filterHugeList(value)); // 非紧急更新:可被延后
});
}
return (
<>
<input value={keyword} onChange={onChange} />
{isPending && <span>更新中...</span>}
<List data={list} />
</>
);
}
解决的问题
text
1. 让输入、点击等用户交互优先响应
2. 大列表、复杂图表、路由切换等低优先级更新可以延后
3. 避免页面长时间被渲染任务阻塞
7.4 useDeferredValue
useDeferredValue 用于延迟某个值的更新,常用于搜索、过滤、大列表渲染。
jsx
import { useDeferredValue, useMemo, useState } from 'react';
function SearchList({ items }) {
const [keyword, setKeyword] = useState('');
const deferredKeyword = useDeferredValue(keyword);
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(deferredKeyword));
}, [items, deferredKeyword]);
return (
<>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<List items={filteredItems} />
</>
);
}
区别:
text
keyword:立即更新,保证输入流畅
deferredKeyword:延后更新,降低重渲染压力
7.5 useId
useId 用于生成服务端和客户端一致的稳定 ID,常用于表单可访问性。
jsx
import { useId } from 'react';
function NameField() {
const id = useId();
return (
<div>
<label htmlFor={id}>用户名</label>
<input id={id} />
</div>
);
}
解决的问题
SSR 场景中,如果客户端和服务端生成的 ID 不一致,会导致 hydration mismatch。
useId 能保证同一组件树结构下服务端和客户端 ID 稳定一致。
7.6 useSyncExternalStore
这个 Hook 主要面向状态管理库作者,也可用于订阅外部 Store。
jsx
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function OnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return <div>{isOnline ? '在线' : '离线'}</div>;
}
解决的问题
并发渲染中,如果外部 Store 在渲染过程中变化,可能出现 tearing,也就是同一次 UI 渲染中不同组件读到的状态不一致。
useSyncExternalStore 通过标准订阅和快照读取协议,让 React 能安全地读取外部状态。
7.7 useInsertionEffect
useInsertionEffect 主要给 CSS-in-JS 库使用,用于在 DOM mutation 后、layout effect 前插入样式。
jsx
import { useInsertionEffect } from 'react';
function useCSS(rule) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => style.remove();
}, [rule]);
}
解决的问题
text
1. 避免 CSS-in-JS 样式插入太晚导致闪烁
2. 保证布局计算前样式已经存在
3. 适配并发渲染下更严格的提交顺序
普通业务代码通常不需要使用它。
7.8 Suspense SSR Streaming
React 18 增强了服务端渲染能力,支持流式 SSR 和选择性 Hydration。
传统 SSR:
text
服务端必须等所有数据准备好
↓
生成完整 HTML
↓
一次性返回给浏览器
↓
浏览器下载 JS
↓
整页 Hydration
React 18 Streaming SSR:
text
先返回页面壳和已准备好的内容
↓
慢数据区域显示 fallback
↓
数据完成后继续通过流发送 HTML
↓
浏览器可以选择性 Hydration 可交互区域
Node 端 API:
js
import { renderToPipeableStream } from 'react-dom/server';
const stream = renderToPipeableStream(<App />, {
onShellReady() {
response.setHeader('content-type', 'text/html');
stream.pipe(response);
}
});
8. React 18 解决了什么问题
8.1 解决大更新阻塞交互
React 旧同步渲染模型中,一旦开始渲染,就会一直执行直到完成。大组件树更新可能阻塞输入和点击。
React 18 并发渲染允许:
text
低优先级渲染任务执行中
↓
用户输入触发高优先级更新
↓
React 暂停低优先级任务
↓
优先处理输入更新
↓
之后继续低优先级渲染
8.2 解决异步更新批处理不一致
React 18 自动批处理让 React 事件、Promise、setTimeout、原生事件中的多个 setState 行为更加一致。
8.3 解决 SSR 首屏等待过长
Streaming SSR 允许服务端先发送页面壳,慢组件后续流式补充,改善首屏体验。
8.4 解决外部 Store 并发安全问题
useSyncExternalStore 为 Redux、Zustand 等外部状态库提供并发安全协议。
9. React 18 核心原理
9.1 Fiber 架构与可中断渲染
React Fiber 可以把渲染工作拆成一个个小单元。
text
组件树渲染任务
↓
Fiber 节点 work unit
↓
执行一部分
↓
检查是否有更高优先级任务
↓
继续 / 暂停 / 丢弃 / 重做
React 18 的并发特性依赖这种可中断架构。
9.2 Render 阶段和 Commit 阶段
React 更新分两大阶段:
text
Render 阶段:
计算新的 Fiber 树,可以被中断、重试、丢弃。
Commit 阶段:
把变更真正提交到 DOM,必须同步完成,不能中断。
并发渲染主要发生在 Render 阶段。
9.3 优先级调度
不同更新有不同优先级:
text
高优先级:输入、点击、光标移动
中优先级:普通状态更新
低优先级:transition、大列表过滤、路由内容切换
React Scheduler 会根据优先级决定先执行哪个任务。
9.4 Transition 原理
startTransition 会把内部状态更新标记为非紧急更新。
jsx
startTransition(() => {
setPage(nextPage);
});
React 知道这个更新可以延后,不应该阻塞输入和点击。
9.5 自动批处理原理
React 18 将更多异步上下文中的状态更新纳入统一批处理范围。
text
多个 setState
↓
进入同一批更新队列
↓
一次调度
↓
一次 render
↓
一次 commit
10. React 18 如何使用与升级
10.1 安装
bash
npm install react@18 react-dom@18
10.2 替换入口渲染方式
旧:
jsx
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
新:
jsx
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
SSR hydration:
jsx
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
10.3 StrictMode 下 effect 执行两次
React 18 开发环境中,StrictMode 会故意重复挂载组件,用于发现副作用问题。
text
开发环境 StrictMode:
mount → unmount → mount
这不是生产行为,而是为了暴露:
text
1. effect 中没有清理订阅
2. 请求重复发送
3. 定时器没有清理
4. 组件不具备可重复挂载能力
10.4 升级检查清单
text
[ ] 将 ReactDOM.render 替换为 createRoot
[ ] 检查自动批处理导致的 DOM 读取时机变化
[ ] 检查 StrictMode 下 effect 是否幂等
[ ] 外部 Store 使用 useSyncExternalStore 或升级状态库
[ ] 大列表或重计算场景考虑 useTransition/useDeferredValue
[ ] SSR 项目评估 renderToPipeableStream/hydrateRoot
[ ] 测试库升级到支持 React 18 的版本
11. React 19 新增了什么
React 19 继续沿着 React 18 的并发基础设施往上走,重点增强异步交互、表单、资源加载、Server Components 和开发体验。
注意:React 19 的很多能力在框架中使用体验更完整,例如 Next.js、Remix 等。纯 CSR 项目也能使用部分客户端能力。
11.1 Actions
Actions 是 React 19 的核心概念之一,用于处理异步数据变更,例如表单提交、保存配置、发送评论、更新购物车等。
传统写法:
jsx
function ProfileForm() {
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
setPending(true);
setError(null);
try {
await updateProfile(new FormData(e.currentTarget));
} catch (err) {
setError(err.message);
} finally {
setPending(false);
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
React 19 可以把异步函数作为 Action 使用。
jsx
function ProfileForm() {
async function submitAction(formData) {
await updateProfile(formData);
}
return (
<form action={submitAction}>
<input name="name" />
<button type="submit">保存</button>
</form>
);
}
解决的问题
text
1. 表单提交样板代码太多
2. pending/error/result 状态分散
3. 异步更新和 Transition 结合不够自然
4. 乐观更新、错误回滚、提交状态缺少统一模型
11.2 useActionState
useActionState 用于管理 Action 的返回状态、提交状态和错误处理。
jsx
import { useActionState } from 'react';
async function updateName(prevState, formData) {
const name = formData.get('name');
if (!name) {
return { error: '请输入用户名' };
}
await saveName(name);
return { message: '保存成功' };
}
function NameForm() {
const [state, formAction, isPending] = useActionState(updateName, null);
return (
<form action={formAction}>
<input name="name" />
<button disabled={isPending}>保存</button>
{state?.error && <p>{state.error}</p>}
{state?.message && <p>{state.message}</p>}
</form>
);
}
原理理解
text
表单提交
↓
调用 formAction
↓
React 标记当前 Action 为 pending
↓
执行异步函数
↓
拿到返回结果
↓
更新 state
↓
结束 pending
11.3 useFormStatus
useFormStatus 用于在表单子组件中读取当前表单提交状态。
jsx
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
);
}
function Page() {
async function submit(formData) {
await save(formData);
}
return (
<form action={submit}>
<input name="title" />
<SubmitButton />
</form>
);
}
解决的问题
提交按钮往往封装在很深的组件中,以前需要层层传递 pending。useFormStatus 可以直接读取最近表单的提交状态。
11.4 useOptimistic
useOptimistic 用于乐观更新。即用户操作后先立即更新 UI,再等待服务端确认。
jsx
import { useOptimistic, useState } from 'react';
function CommentList({ initialComments }) {
const [comments, setComments] = useState(initialComments);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{ id: 'temp', text: newComment, sending: true }
]
);
async function formAction(formData) {
const text = formData.get('comment');
addOptimisticComment(text);
const savedComment = await saveComment(text);
setComments(current => [...current, savedComment]);
}
return (
<>
<ul>
{optimisticComments.map(comment => (
<li key={comment.id}>{comment.text}{comment.sending ? ' 发送中' : ''}</li>
))}
</ul>
<form action={formAction}>
<input name="comment" />
<button type="submit">发送</button>
</form>
</>
);
}
解决的问题
text
1. 提升交互即时反馈
2. 避免等待服务端响应造成卡顿感
3. 统一乐观状态与真实状态的合并方式
4. 更适合评论、点赞、收藏、消息发送等场景
11.5 use API
React 19 支持 use 读取 Promise 或 Context。
读取 Promise
jsx
import { use } from 'react';
function UserInfo({ userPromise }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
需要配合 Suspense:
jsx
<Suspense fallback={<div>加载中...</div>}>
<UserInfo userPromise={fetchUser()} />
</Suspense>
当 Promise pending 时,组件会"挂起",由最近的 Suspense 展示 fallback;Promise resolve 后继续渲染。
读取 Context
jsx
import { createContext, use } from 'react';
const ThemeContext = createContext('light');
function Button() {
const theme = use(ThemeContext);
return <button className={theme}>按钮</button>;
}
与 useContext 的区别之一是,use 可以在条件语句中调用。
jsx
function Button({ enabled }) {
if (enabled) {
const theme = use(ThemeContext);
return <button className={theme}>按钮</button>;
}
return <button>按钮</button>;
}
注意:这不代表普通 Hooks 可以随便在条件中调用,
useState/useEffect/useMemo等仍需遵守 Hooks 规则。
11.6 ref 作为普通 prop
React 19 支持函数组件直接接收 ref prop,减少 forwardRef 的使用。
React 18 及以前常见写法:
jsx
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
return <input {...props} ref={ref} />;
});
React 19:
jsx
function MyInput({ ref, ...props }) {
return <input {...props} ref={ref} />;
}
使用:
jsx
function Form() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
}
解决的问题
text
1. forwardRef 样板代码多
2. 高阶组件和类型推导更复杂
3. ref 与普通 props 心智模型割裂
11.7 Context Provider 简写
React 19 可以直接把 Context 当作 Provider 使用。
旧写法:
jsx
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
新写法:
jsx
<ThemeContext value="dark">
<App />
</ThemeContext>
解决的问题
减少样板代码,让 Context 写法更接近普通组件。
11.8 Document Metadata
React 19 支持在组件中直接渲染文档元信息,如 title、meta、link。
jsx
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.summary} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
React 会把这些标签提升到文档 head 中合适的位置。
解决的问题
以前通常需要 react-helmet 等第三方库管理标题和 meta。React 19 提供内建支持,尤其利于 SSR 和路由页面元信息管理。
11.9 资源预加载 API
React 19 增强资源加载控制能力,提供预加载相关 API。
jsx
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
prefetchDNS('https://cdn.example.com');
preconnect('https://api.example.com');
preload('/font.woff2', { as: 'font', type: 'font/woff2', crossOrigin: '' });
preinit('/main.js', { as: 'script' });
含义:
| API | 作用 |
|---|---|
prefetchDNS |
提前解析域名 DNS |
preconnect |
提前建立连接 |
preload |
提前下载资源 |
preinit |
提前下载并初始化脚本或样式 |
解决的问题
text
1. 提升关键资源加载速度
2. 降低路由切换或组件出现时的等待时间
3. 与 React 渲染过程结合,让资源优先级更明确
11.10 错误处理改进
React 19 改进错误报告方式,减少重复报错,并提供更清晰的 root 错误回调。
jsx
const root = createRoot(document.getElementById('root'), {
onCaughtError(error, errorInfo) {
reportError(error, errorInfo);
},
onUncaughtError(error, errorInfo) {
reportFatalError(error, errorInfo);
}
});
含义:
| 回调 | 含义 |
|---|---|
onCaughtError |
被 Error Boundary 捕获的错误 |
onUncaughtError |
未被 Error Boundary 捕获的错误 |
onRecoverableError |
React 可恢复错误,如 hydration mismatch |
12. React 19 解决了什么问题
12.1 解决异步交互样板代码过多
React 19 之前,表单提交通常要手动维护:
text
pending
error
success
formData
try/catch/finally
disabled button
optimistic update
React 19 用 Actions、useActionState、useFormStatus、useOptimistic 把这些常见模式抽象成标准能力。
12.2 解决 Suspense 与数据读取割裂
use 让 Promise 读取与 Suspense 更自然结合:
text
组件读取 Promise
↓
Promise pending 时挂起
↓
Suspense 展示 fallback
↓
Promise resolve 后继续渲染
12.3 解决 ref 使用复杂
ref 作为普通 prop 简化组件库封装,尤其是 Input、Button、Modal、FormItem 这类基础组件。
12.4 解决文档元信息依赖第三方库
Document Metadata 能在组件内直接声明 title/meta/link,减少对第三方 head 管理库的依赖。
12.5 解决资源加载控制不够统一
资源预加载 API 让 React 应用可以更明确地声明关键资源优先级。
13. React 19 核心原理
13.1 Actions 原理
Action 本质是一个异步函数,但 React 会把它纳入自己的更新调度体系。
text
用户提交表单
↓
React 捕获 form action
↓
执行 Action 异步函数
↓
自动管理 pending 状态
↓
Action 返回结果后更新状态
↓
失败时进入错误处理或返回错误状态
与手写 onSubmit 最大区别:React 知道这是一次异步状态转换,可以与 Transition、Suspense、表单状态联动。
13.2 useActionState 原理
useActionState(action, initialState) 返回:
text
state:上一次 Action 返回的结果
formAction:绑定到 form action 的函数
isPending:当前 Action 是否执行中
每次提交时:
text
prevState + formData
↓
action(prevState, formData)
↓
newState
↓
触发组件更新
13.3 useOptimistic 原理
useOptimistic(baseState, reducer) 会维护一个临时乐观状态。
text
真实状态 baseState
↓ addOptimistic
乐观状态 optimisticState 立即变化
↓ 服务端完成
真实状态更新后,乐观状态与真实状态重新对齐
它不是替代服务端状态,而是在服务端确认前提供临时 UI。
13.4 use 读取 Promise 原理
当组件执行:
js
const data = use(promise);
如果 Promise 还没完成,React 会把这个 Promise 抛给最近的 Suspense 边界。
text
组件渲染
↓
use(promise)
↓
promise pending
↓
组件挂起
↓
Suspense fallback
↓
promise resolved
↓
React 重新渲染组件
这和 Suspense 的底层模型一致:组件通过"抛出 Promise"告诉 React 当前还不能完成渲染。
13.5 ref as prop 原理
React 19 将 ref 从特殊传递机制进一步普通化。函数组件可以直接声明 ref 参数并向下传递。
text
父组件传 ref
↓
React 把 ref 作为 prop 提供给函数组件
↓
函数组件把 ref 绑定到底层 DOM 或子组件
这样组件封装链路更自然。
14. React 19 如何使用与升级
14.1 安装
bash
npm install react@19 react-dom@19
14.2 保持 createRoot
React 19 仍使用 React 18 引入的 root API。
jsx
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root'), {
onCaughtError(error, errorInfo) {
console.error('Caught error', error, errorInfo);
}
}).render(<App />);
14.3 使用 Actions 改造表单
推荐从简单表单开始,不要一次性改全项目。
jsx
async function loginAction(prevState, formData) {
const username = formData.get('username');
const password = formData.get('password');
if (!username || !password) {
return { error: '请输入账号和密码' };
}
await login(username, password);
return { success: true };
}
function LoginForm() {
const [state, action, isPending] = useActionState(loginAction, null);
return (
<form action={action}>
<input name="username" />
<input name="password" type="password" />
<button disabled={isPending}>{isPending ? '登录中...' : '登录'}</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
14.4 使用 useOptimistic 优化体验
适合点赞、收藏、评论:
jsx
function LikeButton({ initialLiked }) {
const [liked, setLiked] = useState(initialLiked);
const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
async function toggleLike() {
setOptimisticLiked(!liked);
const nextLiked = await requestToggleLike(!liked);
setLiked(nextLiked);
}
return (
<button onClick={toggleLike}>
{optimisticLiked ? '已点赞' : '点赞'}
</button>
);
}
14.5 使用 ref as prop 改造组件库
旧组件:
jsx
const Input = forwardRef(function Input(props, ref) {
return <input {...props} ref={ref} />;
});
新组件:
jsx
function Input({ ref, ...props }) {
return <input {...props} ref={ref} />;
}
注意:升级过程中可以继续保留 forwardRef,不需要立刻全量替换。
14.6 升级检查清单
text
[ ] 确认框架和构建工具支持 React 19
[ ] 确认测试库、组件库、状态库兼容 React 19
[ ] 保持 createRoot/hydrateRoot 用法
[ ] 谨慎评估 ref as prop 对 TypeScript 类型的影响
[ ] 从低风险表单开始试用 Actions
[ ] 乐观更新场景优先试用 useOptimistic
[ ] SSR/RSC 项目结合框架文档升级
[ ] 检查废弃 API 和第三方库 peerDependencies
15. Hooks 与 API 演进对比
| API | 引入版本 | 主要用途 | 适合人群 |
|---|---|---|---|
useId |
React 18 | 生成 SSR 稳定 ID | 业务开发者 |
useTransition |
React 18 | 标记非紧急更新 | 业务开发者 |
useDeferredValue |
React 18 | 延迟某个值更新 | 业务开发者 |
useSyncExternalStore |
React 18 | 外部 Store 并发安全订阅 | 状态库作者/高级业务 |
useInsertionEffect |
React 18 | CSS-in-JS 样式插入 | 库作者 |
useActionState |
React 19 | Action 状态管理 | 业务开发者 |
useFormStatus |
React 19 | 表单提交状态 | 业务开发者 |
useOptimistic |
React 19 | 乐观更新 | 业务开发者 |
use |
React 19 | 读取 Promise/Context | 业务开发者/框架作者 |
16. 渲染模式演进:同步、并发、异步资源
16.1 React 17:同步渲染为主
text
状态更新
↓
React 计算组件树
↓
提交 DOM
↓
完成
渲染过程一旦开始通常不能被更高优先级任务打断。
16.2 React 18:并发渲染
text
低优先级更新开始渲染
↓
用户输入发生
↓
React 暂停低优先级渲染
↓
优先处理输入
↓
恢复或重做低优先级渲染
这就是 startTransition、useDeferredValue 背后的核心能力。
16.3 React 19:异步资源融入渲染
React 19 进一步让异步操作和渲染过程结合。
text
组件需要数据或提交表单
↓
Promise / Action 进入 React 调度
↓
Suspense / pending / optimistic UI 接管中间状态
↓
异步完成后更新 UI
17. 表单与异步状态管理演进
17.1 React 18 及以前常见写法
jsx
function Form() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function onSubmit(e) {
e.preventDefault();
setLoading(true);
setError(null);
try {
await save();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
return <form onSubmit={onSubmit}>...</form>;
}
问题:
text
1. 每个表单重复写 loading/error
2. 子按钮组件需要层层传 pending
3. 乐观更新要手动维护临时状态
4. 代码容易遗漏 finally 或异常处理
17.2 React 19 推荐模式
jsx
function Form() {
const [state, action, isPending] = useActionState(saveAction, null);
return (
<form action={action}>
<input name="title" />
<SubmitButton />
{state?.error && <p>{state.error}</p>}
</form>
);
}
子按钮:
jsx
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? '提交中' : '提交'}</button>;
}
收益:
text
1. 表单状态与 Action 绑定
2. 提交状态可以被子组件读取
3. 错误和结果由 Action 返回
4. 更容易与 Server Action、SSR 框架结合
18. 服务端渲染与 Server Components 演进
18.1 React 17 SSR
React 17 SSR 主要是传统整页 HTML 渲染:
text
服务端 renderToString
↓
返回完整 HTML
↓
客户端 hydrate
缺点:慢数据会拖慢整个页面 HTML 返回。
18.2 React 18 Streaming SSR
React 18 支持流式返回:
text
Shell 先返回
↓
Suspense fallback 先展示
↓
慢数据完成后继续注入 HTML
↓
客户端选择性 hydrate
这解决了"必须等所有数据完成才能返回页面"的问题。
18.3 React 19 与 Server Components
React 19 稳定了更多 Server Components 相关能力和底层 API。Server Components 的核心思想是:部分组件只在服务端运行,不发送对应 JS 到客户端。
text
Server Component:
在服务端执行,可以直接读取数据库/文件/服务端资源,不进入客户端 bundle。
Client Component:
在浏览器执行,负责交互、状态、事件。
示意:
jsx
// Server Component
async function ProductPage() {
const products = await db.product.findMany();
return <ProductList products={products} />;
}
// Client Component
'use client';
function AddToCartButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>加入购物车</button>;
}
18.4 使用建议
text
如果是纯后台管理 CSR 项目:
可以重点关注 Actions、表单、乐观更新、ref、资源预加载。
如果是 Next.js 等 SSR/RSC 项目:
可以重点关注 Server Components、Server Actions、use、Streaming SSR。
19. 从 React 17 升级到 18 再到 19 的建议路径
19.1 推荐升级路线
text
第一阶段:React 16 → React 17
目标:解决事件系统兼容,降低大版本升级风险。
第二阶段:React 17 → React 18
目标:切换 createRoot,启用自动批处理和并发基础能力。
第三阶段:React 18 → React 19
目标:逐步使用 Actions、表单新 API、ref as prop 和资源能力。
19.2 不建议一步到位的情况
text
1. 项目历史很久,依赖复杂
2. 有大量第三方组件库
3. 使用老版本路由、状态库、测试库
4. 有微前端多 React 版本共存
5. 有 SSR/hydration 逻辑
这类项目建议先到 React 17,再到 React 18,最后评估 React 19。
19.3 React 17 → 18 风险点
text
1. createRoot 后自动批处理改变部分同步 DOM 读取时机
2. StrictMode 开发环境 effect 重复执行暴露副作用问题
3. 外部 Store 需要并发安全支持
4. 测试中 act 行为可能需要升级测试库
19.4 React 18 → 19 风险点
text
1. 第三方库 peerDependencies 是否支持 React 19
2. ref as prop 对老组件封装和类型定义的影响
3. Actions 与现有表单库的边界
4. SSR/RSC 框架版本是否匹配
5. 废弃 API 或警告是否影响构建
19.5 业务落地优先级
| 优先级 | 能力 | 原因 |
|---|---|---|
| 高 | createRoot | React 18+ 基础入口 |
| 高 | 自动批处理 | 免费性能收益 |
| 高 | StrictMode 副作用治理 | 提升组件稳定性 |
| 中 | useTransition/useDeferredValue | 大列表、搜索、图表场景收益明显 |
| 中 | useId | SSR 或表单可访问性场景稳定 |
| 中 | Actions/useActionState | 表单复杂项目收益明显 |
| 中 | useOptimistic | 社交互动、评论、点赞体验提升 |
| 低 | useInsertionEffect | 主要给库作者 |
| 视项目 | Server Components | 依赖框架和架构选择 |
20. 常见问题与最佳实践
20.1 React 17 是不是没必要升级
不是。React 17 的价值在于降低升级风险,尤其适合大型项目、微前端项目、多 React 版本共存项目。
20.2 React 18 升级后为什么 effect 执行两次
这是开发环境 StrictMode 的故意行为,用来发现副作用不安全问题。生产环境不会这样重复执行。
解决方式不是关闭 StrictMode,而是让 effect 可清理、可重复执行。
jsx
useEffect(() => {
const controller = new AbortController();
fetch('/api/user', { signal: controller.signal });
return () => controller.abort();
}, []);
20.3 自动批处理会不会破坏代码
大多数情况下只会减少渲染次数。但如果代码依赖 setState 后立即读取 DOM,需要使用 flushSync。
jsx
flushSync(() => {
setVisible(true);
});
measureDom();
20.4 什么时候用 useTransition
适合非紧急 UI 更新:
text
1. 搜索结果列表更新
2. 大表格过滤排序
3. 图表重绘
4. 路由切换后的重内容渲染
5. tab 内容切换
不适合:
text
1. 输入框 value 更新
2. 按钮点击反馈
3. 光标、拖拽、选择等必须立即响应的交互
20.5 useDeferredValue 和 debounce 有什么区别
| 对比 | useDeferredValue | debounce |
|---|---|---|
| 控制层 | React 调度层 | 时间延迟层 |
| 是否固定延迟 | 否 | 是 |
| 是否感知渲染优先级 | 是 | 否 |
| 适合场景 | 延迟重 UI 更新 | 降低接口请求频率 |
两者可以组合使用:输入请求用 debounce,列表渲染用 useDeferredValue。
20.6 React 19 的 Actions 是否替代所有表单库
不会。Actions 简化异步提交状态,但复杂表单仍可能需要表单库处理:
text
1. 复杂字段校验
2. 动态表单
3. 字段联动
4. 表单数组
5. Schema 校验
6. 复杂错误展示
Actions 更适合提交状态和异步变更流程,表单库更擅长字段管理。
20.7 use 是否可以替代 useEffect 请求数据
不能简单等价。
text
use + Suspense:更适合渲染期间读取已经创建的 Promise,尤其 SSR/RSC/框架场景。
useEffect:更适合客户端副作用,如埋点、订阅、手动请求、DOM 操作。
20.8 最佳实践总结
text
1. React 17 重点关注平滑升级和事件系统兼容。
2. React 18 重点切换 createRoot,并治理 StrictMode 暴露的副作用问题。
3. 大列表、搜索和路由切换优先使用 useTransition/useDeferredValue。
4. 外部 Store 依赖库要升级到支持 useSyncExternalStore 的版本。
5. React 19 先从表单 Actions、useActionState、useFormStatus、小范围乐观更新开始试点。
6. ref as prop 可以渐进使用,不必立刻替换所有 forwardRef。
7. SSR/RSC 能力优先跟随 Next.js、Remix 等框架最佳实践落地。
8. 大版本升级不要只跑构建,必须覆盖核心交互、表单、路由、弹窗、状态库和测试用例。
总结
React 17、18、19 的主线非常清晰:
text
React 17:先解决大型项目"怎么安全升级"的问题。
React 18:再解决复杂 UI "怎么不卡、怎么可中断、怎么流式渲染"的问题。
React 19:继续解决异步交互、表单、资源、ref、服务端组件"怎么更好写"的问题。
如果要在业务项目中落地,可以按这个顺序:
text
1. 先升级到 React 17,处理事件系统和依赖兼容。
2. 再升级到 React 18,替换 createRoot,享受自动批处理。
3. 对重渲染场景使用 useTransition/useDeferredValue。
4. 最后升级到 React 19,逐步用 Actions 改造表单和异步交互。
5. SSR/RSC 项目结合框架能力使用 use、Server Components 和资源预加载。