React 19 升级优先级评估:哪些新 API 可替代现有的临时解决方案
深入探讨 Server Actions、useOptimistic 以及 React 编译器的实际应用,包括相关注意事项,以及优先采用 React 19 哪些变更的规划。
React 19 拥有多年来最多的 API 新功能,大量的相关文档让人难以判断哪些对现有代码库真正重要。对大多数团队而言,其中一些新功能可以替代现有的临时解决方案,有一个改变了处理数据加载的方式,其余的则可以先暂缓考虑。下文列出了真正重要的功能,附有前后对比代码以及容易被忽略的注意事项,帮助你根据实际价值来规划升级。
use:在渲染过程中读取 Promises 和 Context
use API 是最重要的新增功能,其行为与所有已知钩子都不同。常规钩子必须在组件的顶层按固定顺序在每次渲染时调用,而 use 不受此限制:你可以在提前返回后、条件语句中或循环内部调用它。
在下面的示例中,当没有 userId 时,该组件会返回访客视图,之后才会读取用户信息:
// React 19 — use() can be called inside conditionals and loops
function UserProfile({ userId }) {
if (!userId) return <GuestView />;
// This is valid in React 19
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
真正的优势在于 use 函数可以接收的内容:Promise 或 Context。如果是 Promise,React 会暂停组件的执行直到其状态确定。下面的对比展示了功能减少了多少——旧版本需要在状态中跟踪数据与加载标志,通过效应函数进行请求并手动渲染旋转图标;而 React 19 版本则可以直接读取该值:
// Before React 19
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
// React 19
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
工作流程只是发生了转移而非消失:最近的 <Suspense> 边界会在 Promise 处于待处理状态时显示备用内容,而最近的错误边界则负责处理拒绝情况。该组件仅描述了成功路径。
Promise 必须保持稳定
use 要求在每次渲染时使用相同的 Promise。如果 fetchUser(userId) 在每次渲染时都创建新的 Promise,React 会再次暂停执行,从而导致重复请求或组件永远无法进入稳定状态;React 会对客户端组件中在渲染过程中创建的未缓存 Promise 发出警告。因此,应将这些示例视为概念说明。Promise 应该来自能够缓存它的机制:具备 Suspense 支持的数据库、框架加载器,或是负责发起请求并将 Promise 作为属性传递下来的服务器组件。虽然有时会建议用 useMemo 将该调用包裹起来,但 React 并不保证已缓存的值会被保留,因此它并非可靠的缓存方式。
作为 React 特性的服务器操作
Next.js App Router 的用户已经熟悉 Server Actions。在 React 19 中,它们已成为 React 本身的一部分(文档现在称其为 Server Functions),任何支持 Server Components 的框架都可用。
该示例定义了一个带有 'use server' 标记的异步函数,该函数用于插入用户并重新验证列表,然后将其传递给表单的 action 属性:
// Server Action — runs on the server, called from the client
async function submitForm(formData) {
'use server';
const name = formData.get('name');
await db.users.create({ name });
revalidatePath('/users');
}
// Client component
function UserForm() {
return (
<form action={submitForm}>
<input name="name" />
<button type="submit">Add User</button>
</form>
);
}
<form> 的 action 属性现在可以接受函数,包括异步函数;React 会在表单提交时在过渡阶段执行该函数,并将其与 FormData 一起传递。待处理状态、乐观更新和错误信息则来自相关 API:useFormStatus 或 useActionState 用于处理待处理状态和结果状态,useOptimistic 用于提供即时反馈,而错误边界则用于处理故障。你需要编写数据修改逻辑,其余工作由 React 来协调。
在真实应用中,该代码片段有一个细节需要调整。带有内联 'use server' 指令的函数只能定义在服务器组件中。如果 UserForm 是客户端组件,那么应将 submitForm 移到单独的文件中,并在文件顶部添加 'use server',然后再导入该文件。
服务器组件从客户端打包中移除了静态 UI,而服务器动作则去掉了用于数据变更的手写 API 路由:这样既减少了代码量,也降低了数据传输次数。不过仍需将每个动作视为一个公共端点,并在其中对输入和权限进行验证。
useOptimistic:即时反馈且无状态重复
过去,若要在服务器确认之前就渲染结果,就需要手动管理额外的状态。useOptimistic函数会接收当前状态以及一个类似reducer的更新函数,然后返回可用于渲染的状态以及一个用于应用乐观更新的函数。在这里,添加待办事项时会插入一个标记为“待处理”的临时项,该项在保存完成之前会以稍显淡化的形式显示:
function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, newTodo]
);
async function handleAdd(text) {
addOptimisticTodo({ id: 'temp', text, pending: true });
await saveTodo(text); // Server Action
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.7 : 1 }}>
{todo.text}
</li>
))}
</ul>
);
}
当调用addOptimisticTodo时,React会立即显示乐观状态下的列表。当相关的操作完成后,乐观状态值会被丢弃,React会再次根据todos属性进行渲染,此时该属性中应已包含已保存的待办事项。
以前你需要手动保存已确认和待处理的数据副本,并在数据出现差异时进行清理;现在则由钩子来管理整个生命周期。有两个关键条件:首先,乐观更新必须发生在某个操作或状态转换过程中,例如传递给表单action属性的函数,或被包裹在startTransition中的函数;若从普通的事件处理程序中调用它,React会发出警告,且更新内容可能不会显示。其次,父组件必须在保存操作后真正收到最新的todos数据,通常是通过重新验证实现的,否则当乐观状态重置时新添加的项就会消失。如果快速添加两个项目,像'temp'这样的临时ID也可能会冲突,因此需要生成唯一的ID。我们在关于useOptimistic五种回滚失败模式的文章中详细介绍了这些边缘情况。
React 编译器:默认启用记忆化
React 编译器(原名 React Forget)随 React 19一同推出,作为可选的构建步骤,它对日常代码的影响最为深远。该工具会在构建时插入记忆化机制,这样当输入不变时就会重用值和回调函数,而当属性相同则子组件无需重新渲染。如对比所示,手动使用 useMemo、useCallback 和 React.memo 在大多数情况下已不再必要:
// Before: manual memoization required
const expensiveValue = useMemo(() => compute(a, b), [a, b]);
const stableCallback = useCallback(() => doSomething(id), [id]);
const MemoizedChild = React.memo(ChildComponent);
// After React Compiler: write normal code
const expensiveValue = compute(a, b);
const handleClick = () => doSomething(id);
// ChildComponent renders only when its props change - automatically
你仍然需要了解组件何时以及为何会重新渲染,但那些手动解决方案的重要性已大大降低。我们关于导致不必要的重新渲染的模式的概述仍可作为有用的参考资料。
对于新项目,使用编译器是稳妥的默认选择。在现有代码中则需谨慎引入:该编译器假设组件是纯函数且遵循 React 规范,因此那些在渲染过程中会发生变化的代码在经过编译后行为可能会不同。应逐步启用该功能,先运行测试,并使用 ESLint 插件来查找违规之处。可查阅最新的 React 文档,了解其发布状态及支持的 React 版本。
应采用哪些功能,以及顺序如何
即时见效,风险较低
- 在表单与服务器状态交互的地方使用
useOptimistic。 - 如果使用的是 Next.js 14 或更高版本,且希望用 Server Actions 替代用于数据修改的 API 路由,则可选用该方案。
值得规划的未来功能
- 当数据层能够生成稳定且可缓存的 Promise 时,可使用
use;它与 Suspense 能很好地配合使用。
对大多数应用而言并非紧急事项
- ref 的变化:现在可以将
ref作为普通属性传递给函数组件,因此不再需要forwardRef。 - 上下文功能的改进,这些主要是开发者体验方面的变化,而非新的行为特性。
- 文档元数据支持,使组件能够渲染 React 放在文档头部的
<title>和<meta>标签。
核心要点
- React 19 是一种演进而非彻底的重写。其新 API 将生产团队早已手动实现的模式进行了规范化:乐观更新、服务器端数据修改以及条件性数据读取。
use将加载和错误处理功能移至 Suspense 与错误边界,但仅适用于在渲染之外缓存过的 Promise。- Server Actions 与
useOptimistic非常契合:前者负责处理数据写入,后者则用于隐藏其延迟。 - 只要组件遵循 React 的规则,编译器就会默认启用记忆化功能。
关键问题不在于是否需要升级,而在于这些新特性中哪一种可以替代你目前正在使用的临时解决方案。请从这里入手。
相关阅读
- useOptimistic Rollback:Next.js Server Actions中的五种故障模式 — 通过五种经过测试的Server Action故障模式及可行的解决方案,了解为何useOptimistic会在不向用户说明故障原因的情况下悄悄恢复UI。
- Server Actions还是Route Handlers?Next.js 16的决策指南 — 了解在何种情况下Next.js 16中的mutation应使用Server Action,又在何种情况下需要Route Handler,并提供表单、错误处理、乐观更新及Webhook相关的修正示例。