React 19 新特性全解:Actions、use()、编译器与并发改进
第28章:React 19 新特性全解:Actions、use()、编译器与并发改进
React 19 围绕一个核心主题展开:让代码更接近'描述意图'而不是'描述实现'。
本章核心问题:React 19 的 Actions 模型、use() Hook 和 Compiler 分别解决了什么问题? 读完本章你将理解:
- Actions + useActionState + useOptimistic 让异步表单处理从命令式变为声明式
- use() 可以在条件语句中使用,统一了消费 Context 和 Promise 的 API
- React Compiler 在编译时自动插入记忆化,前提是代码遵守纯度规则
Level 1 · 你需要知道的(1-3年经验)
Actions:异步操作的声明式模型
问题:表单提交的状态管理噩梦
在 React 18 时代,处理一个异步表单提交需要手动管理大量状态:
// React 18 中的典型异步表单处理
function UpdateNameForm() {
const [name, setName] = useState('');
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
const [optimisticName, setOptimisticName] = useState('');
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
setOptimisticName(name); // 乐观更新
try {
await updateName(name);
} catch (err) {
setError(err.message);
setOptimisticName(''); // 回滚乐观更新
} finally {
setIsPending(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
{isPending && <Spinner />}
{error && <p>{error}</p>}
</form>
);
}
这段代码并不复杂,但每个表单都要重复这个模式,不仅繁琐,而且容易遗漏错误处理。
Actions:用异步函数替代事件处理器
React 19 引入了 Actions 的概念:可以将异步函数直接传给表单的 action 属性,React 会自动管理 pending 状态、错误状态和乐观更新:
// React 19 的 Actions 模型
function UpdateNameForm() {
const [error, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const name = formData.get('name');
const error = await updateName(name);
if (error) return error;
return null;
},
null // 初始状态
);
return (
<form action={submitAction}>
<input name="name" />
{isPending && <Spinner />}
{error && <p>{error}</p>}
<button type="submit" disabled={isPending}>Update</button>
</form>
);
}
useActionState 接收一个异步函数,返回 [state, action, isPending] 元组。当表单提交时,React 自动将 isPending 设为 true,等待异步函数完成后更新 state,并将 isPending 重置为 false。
useFormStatus:读取父表单状态
useFormStatus 允许子组件读取其所属表单的提交状态,无需通过 props 传递:
// packages/react-dom/src/client/ReactDOMFormElement.js 提供支持
import { useFormStatus } from 'react-dom';
function SubmitButton() {
// 自动读取最近的父 <form> 的状态
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={someAction}>
<input name="data" />
<SubmitButton /> {/* 自动获取表单 pending 状态 */}
</form>
);
}
useFormStatus 底层使用 Context 机制,React 在处理 <form action={fn}> 时会创建一个 FormStatus Context,所有子组件可以通过 useFormStatus 订阅它。
useOptimistic:乐观更新的声明式实现
import { useOptimistic } from 'react';
function MessageList({ messages, sendMessage }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(currentMessages, newMessage) => [
...currentMessages,
{ text: newMessage, status: 'sending' }
]
);
async function formAction(formData) {
const message = formData.get('message');
// 立即显示乐观消息(不等待服务器响应)
addOptimisticMessage(message);
// 发送到服务器
await sendMessage(message);
// 服务器响应后,optimisticMessages 自动回退到 messages(真实数据)
}
return (
<div>
{optimisticMessages.map((msg, i) => (
<p key={i} style={{ opacity: msg.status === 'sending' ? 0.7 : 1 }}>
{msg.text}
</p>
))}
<form action={formAction}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}
useOptimistic 的实现原理是:在 action 执行期间,它使用"乐观状态"(通过 addOptimisticMessage 应用的中间状态)替代真实状态进行渲染。当 action 完成时(无论成功还是失败),React 自动切换回真实状态,丢弃乐观状态。这个切换发生在一个 transition 中,所以不会引起页面闪烁。
use():在渲染中读取资源
use() 是 React 19 中最具革命性的 Hook——它打破了以往"只能在 React 组件函数顶层调用 Hook"的惯例,可以在条件语句、循环,乃至普通函数中调用。
读取 Promise
import { use, Suspense } from 'react';
// 在组件外创建 Promise(不能在组件内创建,避免每次渲染重新创建)
const userPromise = fetchUser(userId);
function UserProfile() {
// use() 读取 Promise — 如果 Promise 还未 resolve,组件会 suspend
const user = use(userPromise);
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
);
}
use(promise) 的工作机制与 throw promise(Suspense 的传统触发方式)类似,但更加简洁:当 Promise 还未 resolve 时,use() 会抛出 Promise,触发最近的 Suspense 边界显示 fallback;当 Promise resolve 后,React 重新渲染组件,use() 直接返回 resolved 的值。
// use() 可以在条件语句中使用
function DataDisplay({ condition, promise1, promise2 }) {
const data = use(condition ? promise1 : promise2);
return <div>{data}</div>;
}
读取 Context
use() 也可以读取 Context,与 useContext 等价,但可以在条件语句中使用:
function Button({ show }) {
if (!show) return null; // 可以在这之后调用 use()
const theme = use(ThemeContext); // 等价于 useContext(ThemeContext)
return <button style={{ color: theme.color }}>Click</button>;
}
这个特性让之前许多需要重构组件结构的场景变得简单——你不再需要为了"有条件地使用 Context"而把组件拆成两个。
use() 的内部实现
// packages/react/src/ReactHooks.js(简化)
export function use(usable) {
return ReactCurrentDispatcher.current.use(usable);
}
// packages/react-reconciler/src/ReactFiberHooks.js
function useThenable(thenable) {
// 追踪当前 thenable 的状态
const index = thenableIndexCounter++;
const thenableState = getThenableStateAfterSuspending();
// 检查这个 Promise 是否已经 resolve
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedReason = thenable.reason;
throw rejectedReason;
}
default: {
const prevThenableAtIndex = thenableState !== null
? thenableState.get(index)
: null;
if (prevThenableAtIndex === thenable) {
// 同一个 Promise,检查状态
switch (thenable.status) {
case 'fulfilled': return thenable.value;
case 'rejected': throw thenable.reason;
}
}
// 订阅 Promise 状态变化
switch (thenable.status) {
case 'pending':
thenable.then(
(fulfilledValue) => {
if (thenable.status === 'pending') {
thenable.status = 'fulfilled';
thenable.value = fulfilledValue;
}
},
(error) => {
if (thenable.status === 'pending') {
thenable.status = 'rejected';
thenable.reason = error;
}
}
);
break;
}
// 抛出 Promise,触发 Suspense
throw thenable;
}
}
}
React Compiler:自动记忆化
React Compiler(前身是 React Forget 项目)是 React 19 生态中最具野心的工程:一个将 React 组件的手动 useMemo/useCallback 优化自动化的编译器。
工作原理
React Compiler 在构建时(而非运行时)分析组件代码,判断哪些值和函数在什么条件下保持稳定,并自动插入记忆化逻辑。
// 你写的代码
function ProductList({ category, onSelect }) {
const filteredProducts = products.filter(p => p.category === category);
return filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onSelect={onSelect}
/>
));
}
// React Compiler 编译后的输出(概念性展示)
function ProductList({ category, onSelect }) {
const $ = useMemoCache(3);
let filteredProducts;
if ($[0] !== category) {
filteredProducts = products.filter(p => p.category === category);
$[0] = category;
$[1] = filteredProducts;
} else {
filteredProducts = $[1];
}
let result;
if ($[2] !== filteredProducts || $[3] !== onSelect) {
result = filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onSelect={onSelect}
/>
));
$[2] = filteredProducts;
$[3] = onSelect;
$[4] = result;
} else {
result = $[4];
}
return result;
}
编译器使用了一个名为 useMemoCache 的内部 Hook(不对外公开)来存储缓存值。它分析组件的数据流图,识别哪些计算依赖于哪些 props/state,然后只在依赖变化时重新计算。
组件纯度假设
React Compiler 的工作前提是组件和 Hook 必须是纯的(相同输入,相同输出,无副作用)。编译器会分析代码并检测潜在的不纯操作。如果检测到不纯,编译器会跳过对该组件的优化,而不是生成错误的代码。
// 编译器会跳过优化:直接修改 props 或外部变量
function BadComponent({ items }) {
items.push('new item'); // 违反纯度,编译器会放弃优化
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}
Level 2 · 它是怎么运行的(3-5年经验)
ref 作为 prop:告别 forwardRef
React 19 中,函数组件可以直接接收 ref 作为 prop,不再需要 forwardRef 包装:
// React 18:需要 forwardRef
const MyInput = forwardRef(function MyInput({ placeholder }, ref) {
return <input ref={ref} placeholder={placeholder} />;
});
// React 19:直接接收 ref prop
function MyInput({ placeholder, ref }) {
return <input ref={ref} placeholder={placeholder} />;
}
// 使用方式相同
function Parent() {
const ref = useRef(null);
return <MyInput ref={ref} placeholder="Enter text" />;
}
在 React 19 内部,当渲染函数组件时,如果组件的 props 中有 ref 字段,React 会直接将其传入,不再需要中间的包装层。forwardRef 在 React 19 中仍然有效(向后兼容),但官方建议逐步迁移到直接 prop 模式。
文档元数据:在组件中声明 title 和 meta
React 19 原生支持在组件中声明文档元数据,无需第三方库:
function BlogPost({ post }) {
return (
<article>
{/* 这些标签会被自动提升到 <head> */}
<title>{post.title}</title>
<meta name="description" content={post.summary} />
<link rel="canonical" href={post.url} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
React 19 的渲染器会识别 <title>、<meta>、<link> 等特殊标签,并将它们"提升"(hoist)到文档的 <head> 部分。在服务端渲染(SSR)场景下,这些标签会在 HTML 输出时被正确放置在 <head> 中;在客户端渲染场景下,React 会使用 DOM API 将它们插入 <head>。
当同一个元数据标签出现在多个组件中时,React 会进行去重——例如,多个组件都声明了 <title>,页面上只会呈现最后一个(或优先级最高的那个)。
资源预加载 API
React 19 新增了一组资源加载 API,可以在组件中声明式地预加载关键资源:
import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';
function VideoPlayer({ src }) {
// 预加载视频资源(以更高优先级获取,但不立即执行)
preload(src, { as: 'video' });
// preinit: 预加载并立即执行(适用于关键脚本和样式)
preinit('https://cdn.example.com/player.js', { as: 'script' });
return <video src={src} controls />;
}
function App() {
// DNS 预解析和预连接
prefetchDNS('https://api.example.com');
preconnect('https://api.example.com', { crossOrigin: 'anonymous' });
return <VideoPlayer src="/video.mp4" />;
}
这些 API 最终生成 <link rel="preload">、<link rel="prefetch"> 等 HTML 标签,帮助浏览器更早地获取关键资源,改善页面加载性能。在 SSR 场景下,这些提示会被写入初始 HTML,让浏览器在解析 HTML 时就开始并行加载资源。
Level 3 · 规范怎么定义的(资深)
React 18 → 19 迁移指南
破坏性变更
1. 移除的遗留根 API
// 已移除(React 18 已废弃)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, container); // ❌
ReactDOM.hydrate(<App />, container); // ❌
ReactDOM.unmountComponentAtNode(container); // ❌
// 使用新 API
import { createRoot, hydrateRoot } from 'react-dom/client';
const root = createRoot(container);
root.render(<App />); // ✅
2. 移除 propTypes 运行时检查
// React 19 完全移除了运行时 propTypes 验证
MyComponent.propTypes = { ... }; // 不再有任何效果,建议改用 TypeScript
3. 移除 defaultProps(函数组件)
// React 19 中,函数组件的 defaultProps 被移除
// 改为使用 ES 默认参数
function Button({ color = 'blue', size = 'medium' }) {
return <button className={`btn-${color} btn-${size}`}>Click</button>;
}
// 注意:类组件的 defaultProps 仍然保留
4. ref 回调的清理函数
// React 19 中,ref 回调可以返回清理函数
function Component() {
return (
<div
ref={(node) => {
// setup
if (node) {
node.addEventListener('click', handler);
}
// cleanup(React 19 新增)
return () => {
node.removeEventListener('click', handler);
};
}}
/>
);
}
5. Context 提供方写法简化
// React 18
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
// React 19 可以直接使用 Context 本身作为 Provider
<ThemeContext value={theme}>
{children}
</ThemeContext>
迁移建议优先级
| 优先级 | 变更 | 工作量 |
|---|---|---|
| 必须 | 替换 ReactDOM.render 为 createRoot |
低 |
| 必须 | 删除 propTypes |
中(建议迁移到 TypeScript) |
| 建议 | 将 forwardRef 替换为直接 prop |
中 |
| 建议 | 将 Context.Provider 替换为 Context |
低 |
| 可选 | 使用 use() 替换 useContext |
低 |
| 可选 | 采用 Actions 模式替换手动表单状态管理 | 高(收益大) |
React 19 的这些改进不是孤立的功能点,而是围绕一个核心主题展开的:让 React 代码更接近"描述意图"而不是"描述实现"。Actions 让你描述"提交这个表单"而不是管理 pending/error 状态;Compiler 让你描述"这是组件的渲染逻辑"而不是手动优化哪些值应该记忆化;文档元数据让你在组件中声明"这个页面的标题是什么"而不是调用外部库的命令式 API。这是 React 设计哲学的持续演进。
Level 4 · 边界与陷阱(所有人)
改进的错误报告与 hydration 错误提示
更好的错误分组
React 19 改进了错误捕获机制。在之前的版本中,一个错误可能在控制台触发多次输出(react 内部处理一次,window.onerror 再捕获一次)。React 19 统一了错误报告路径:
// React 19 中,新增了两个 root 选项
const root = createRoot(container, {
// 可恢复的错误(有 ErrorBoundary 捕获)
onRecoverableError: (error, errorInfo) => {
console.error('Caught by ErrorBoundary:', error, errorInfo.componentStack);
},
// 不可恢复的错误(没有 ErrorBoundary 捕获)
onUncaughtError: (error, errorInfo) => {
console.error('Uncaught error:', error, errorInfo.componentStack);
},
});
Hydration 错误的差异展示
React 19 在 hydration 错误时会显示服务端与客户端渲染结果的具体差异,而不仅仅是"hydration 失败"这样的模糊提示:
Hydration failed because the server rendered HTML didn't match the client.
Server: <div class="container">Hello</div>
Client: <div class="container">World</div>
这让定位和修复 SSR 不一致问题变得直接得多。