移除提供者包装层:来自服务器组件的渲染上下文
了解 React 19.3 如何让服务器组件直接渲染客户端上下文,Flight 是如何对其进行序列化的,以及目前仍存在的读取限制和值的相关约束。
来源: React 19.3: Render a Context Directly in a Server Component, No Provider Wrapper
几乎所有的 React Server Components 代码库中都存在一个很小的 'use client' 文件,它的唯一作用就是创建一个 Context 并导出一个提供者组件,该组件用于传递单个属性。之所以需要这个包装器,是因为服务器组件原本不允许直接渲染 Context 本身。React 19.3 取消了这一限制,因此服务器现在可以直接渲染 Context 元素,也就无需再使用该包装器了。
本指南将详细介绍旧模式与新模式,深入讲解Flight协议在底层的具体功能,以及那些决定该变更是否能在您当前框架中正常运行的关键问题。这些示例需要真实的Server Components运行时环境(即支持RSC打包工具且具备服务器功能的框架),因此请将其视为完整的参考代码,而非可直接粘贴到测试环境中的内容。如需了解RSC应用中服务器端与客户端端如何协同工作的背景知识,请参阅我们关于React Server Components背后架构的概述。
旧模式:仅用于转发属性的组件
服务器组件无法调用createContext,因为上下文属于客户端状态管理机制。然而,其下层的客户端组件常常需要服务器已有的数据,比如已登录的用户信息、租户的功能开关或请求的地域设置。跨越这一界限的唯一方式是通过客户端组件的属性传递数据,再由该客户端组件渲染提供者。
客户端模块以常规的指令和导入语句开始。
'use client';
import { createContext } from 'react';
接着它会创建上下文,并导出一个提供者,该提供者的功能仅是将currentUser作为value传递出去。
export const UserContext = createContext(null);// exists only to forward one prop
export function UserProvider({ currentUser, children }) {
return (
<UserContext value={currentUser}>
{children}
</UserContext>
);
}
服务器组件端
布局部分作为服务器组件,导入的是包装器而非上下文。
import { UserProvider } from './user-context';
它在服务器端获取用户信息,然后以属性的形式将其传递给包装器。
export async function Layout({ children }) {
const currentUser = await getCurrentUser();
return (
<UserProvider currentUser={currentUser}>
{children}
</UserProvider>
);
}
如果在 React 19.2 中试图跳过包装器,就会遇到一个令人困惑的运行时错误,错误信息以“Element type is invalid”开头,随后指出某个懒加载的元素类型被解析为 Context 而非类或函数。这看似是打包器的问题,但实际上原因在于渲染器。跨客户端边界导入的 Context 会以客户端引用的形式传递给服务器端渲染器,而这些引用是延迟解析的,19.2 版本只能将这些引用转换为组件,而 Context 对象从来都不是被接受的结果。
新的方案:直接渲染 Context 本身
在 19.3 版本中,客户端模块仅包含指令、导入语句以及一个导出语句。
'use client';
import { createContext } from 'react';
不再有组件存在,只有 Context 对象。
export const UserContext = createContext(null);
从布局中渲染它
现在布局会直接导入 UserContext。
import { UserContext } from './user-context';
JSX的结构与之前相同,不同之处在于元素为Context,而属性名为value。
export async function Layout({ children }) {
const currentUser = await getCurrentUser();
return (
<UserContext value={currentUser}>
{children}
</UserContext>
);
}
在客户端使用它
对于使用者而言没有任何变化。布局下方的任何客户端组件都会导入同一个Context,并通过use来读取它。
'use client';
import { use } from 'react';
import { UserContext } from './user-context';
在这里,头像菜单从Context中获取用户信息并渲染图像。
export function AvatarMenu() {
const user = use(UserContext);
return <img src={user.avatar} alt={user.name} />;
}
注意布局中的React 19语法:它渲染的是<UserContext value>而非<UserContext.Provider value>。虽然使用.Provider形式在客户端仍然有效,但服务器端会直接渲染Context对象本身,因此请在服务器代码中使用更简短的形式。
Flight的功能以及该特性不具备的功能
当服务器组件渲染 <UserContext value={currentUser}> 时,服务器端根本不会执行任何上下文逻辑。Flight序列化器会生成一个类型为客户端引用的元素,该引用指向模块 user-context.js 及其导出的 UserContext 对象。该元素的属性为 { value: currentUser, children },这些属性的序列化方式与传递给客户端的其它属性完全相同。一旦浏览器加载了该模块,这个客户端引用就会转化为实际的上下文对象,React随后会创建一个提供者来保存所传递的值。在 facebook/react #35675 中记录的渲染器修复方案让懒加载类型的代码路径能够接受上下文对象,而不仅仅是函数、memo 和 forwardRef。
这种设计带来了两个后果。
该值必须是可序列化的
Context 的值遵循与任何传递到客户端的属性相同的规则。允许的类型包括字符串和数字等原始类型、普通对象和数组、Date、Map 和 Set 实例、promise、JSX 元素以及 Server Functions。普通函数、类实例以及具有自定义原型的任何对象均不在允许范围内。单纯的 currentUser 记录是可行的,而带有方法的 UserService 实例则不可行。'use client' 文档中列有完整列表。
这不是服务器端的 Context
拉取请求中明确指出,仍然不能在 Server Component 内部调用 use(Context)。Server Components 无法读取 Context,因为服务器端不存在可供它们读取的提供者。该功能纯粹是为了更快捷地将值传递给客户端树而设计的。
为何值得移除封装层
过去,每个共享值都需要一个文件和一个组件。起初这似乎只是表面上的需求,但实际统计起来却相当可观:一个包含用户上下文、主题、区域设置、功能标志、租户信息以及分析功能的应用,需要六个几乎相同的封装结构,而每一个都可能让人忘记某个属性或误输默认值。采用新形式后,数据获取位置就在提供者所在处,于是“哪些部分在服务器运行、哪些在客户端运行”这种思维模式也就不再有特殊情况了。
社区对这一变化的反应大多是如释重负,而非兴奋。开发者们认为,这种功能最初居然没有得到支持实在奇怪,因为上下文值本质上只是存储在内存中的数据。这种直觉是正确的,这也解释了为什么在 React 本身中的修复工作如此简单。
迁移前的注意事项
- 您的框架必须提供 19.3 版本的 Flight 包。相关更改位于
react-server-dom-webpack、-turbopack和-parcel以及对应的协调模块中,而不仅仅在react中。请检查您的框架所指定的 React 版本;版本不匹配会导致与之前相同的“Element type is invalid”错误。 - 应在标有
'use client'的文件中定义 Context。在服务器端代码中定义的 Context 无法以这种方式渲染,因为整个机制依赖于该对象存在于客户端打包文件中。 - 在服务器端仍然无法进行读取操作。需要该值的服务器组件应通过参数接收它或自行获取;在此情况下使用
use(Context)会抛出错误。
await而直接写入<UserContext value={getCurrentUser()}>,就会直接传输该值,而接收方必须通过use(use(UserContext))两次解包才能使用它。这虽然是一种有用的流式处理技术,但也很容易因为遗漏await而出错。createContext(null)的默认值会在没有提供者在接收者之上的所有情况下生效,而在服务器端渲染提供者并不会改变这一情况。暂时继续使用包装器
如果您的框架尚未升级到19.3版本,可继续使用封装函数。UserProvider模式在所有React 19版本及各类框架中均适用。升级时迁移过程十分简单:
- 从客户端模块中删除封装函数。
- 在服务端组件中直接导入Context,而非使用封装函数。
- 将传递的属性重命名为
value。
随着框架逐步升级到19.3版本,您可以逐个文件进行迁移。无需一次性转换所有内容,且两种实现方式在运行时并无差异。
关键要点
- 在React 19.3版本中,服务端组件可以直接渲染从
'use client'模块导出的Context,因此专门用于封装提供器的函数已不再必要。