This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
在 Expo Router 应用中使用 React 服务端组件
编辑页面
了解如何在 Expo 中于服务器端渲染 React 组件。
React Server Components 支持许多令人兴奋的能力,包括:
- 使用异步组件和 React Suspense 进行数据获取。
- 使用密钥和服务端 API。
- 用于 SEO 和性能的服务端渲染(SSR)。
- 构建时渲染,以移除未使用的 JS 代码。
Expo Router 在所有平台上都支持 React Server Components。这是该功能的早期 预览,未来将会在 Expo Router 中默认启用。
2 requirements
2 requirements
1.
如果你还没有,请查看 Expo Router 安装指南。
2.
需要 React Native New Architecture,并且从 SDK 52 起默认启用。
用法
要在 Expo 应用中使用 React Server Components,你需要:
-
安装所需的 RSC 依赖:
Terminal-npx expo install react-server-dom-webpack -
确保入口模块在 package.json 中是
expo-router/entry(默认)。 -
在项目 app 配置中启用该标志:
{ "expo": { "experiments": { "reactServerFunctions": true } } }
- 确保你的 app 配置中的任何位置都没有将
"origin"设置为布尔值。 - 创建一个初始路由 app/index.tsx:
/// <reference types="react/canary" /> import React from 'react'; import { ActivityIndicator } from 'react-native'; import renderInfo from '../actions/render-info'; export default function Index() { return ( <React.Suspense fallback={ // 在 Server Function 等待数据期间将渲染的视图。 <ActivityIndicator /> }> {renderInfo({ name: 'World' })} </React.Suspense> ); }
- 创建一个 Server Function actions/render-info.tsx:
'use server'; import { Text } from 'react-native'; export default async function renderInfo({ name }) { // 安全地从 API 获取数据,并读取环境变量... return <Text>Hello, {name}!</Text>; }
Server Function 的视图返回值是一个 React Server Component payload,它将被流式传输到客户端。
在开发者预览期间,app 配置中的
web.output必须为single。更多输出模式的支持即将推出。
服务端组件
服务端组件在服务端运行,这意味着它们可以访问服务端 API 和 Node.js 内置模块(在本地运行时)。它们也可以使用异步组件。
考虑下面这个会获取数据并渲染它的组件:
import 'server-only'; import { Image, Text, View } from 'react-native'; export async function Pokemon() { const res = await fetch('https://pokeapi.co/api/v2/pokemon/2'); const json = await res.json(); return ( <View style={{ padding: 8, borderWidth: 1 }}> <Text style={{ fontWeight: 'bold', fontSize: 24 }}>{json.name}</Text> <Image source={{ uri: json.sprites.front_default }} style={{ width: 100, height: 100 }} /> {json.abilities.map(ability => ( <Text key={ability.ability.name}>- {ability.ability.name}</Text> ))} </View> ); }
要将其作为服务端组件渲染,你需要从一个服务端函数中返回它。
重点
- 你不能在服务端组件中使用
useState、useEffect或useContext之类的钩子。 - 你不能在服务端组件中使用浏览器或原生 API。
"use server"并不意味着将文件标记为服务端组件。它用于标记文件中导出了 React 服务端函数。- 服务端组件可以访问所有环境变量,因为它们在客户端之外安全运行。
客户端组件
由于 Server Components 不能访问原生 API 或 React Context,你可以创建一个 Client Component 来使用这些特性。它们通过在文件顶部添加 "use client" 指令来创建。
'use client'; import { Text } from 'react-native'; export default function Button({ title }) { return <Text onPress={() => {}}>{title}</Text>; }
这个模块可以在 Server Function 或 Server Component 中导入并使用。
重点
你不能向 Server Components 传递函数作为 props。你只能传递可序列化的数据。
React 服务器函数
服务器函数是在服务器上运行,并且可以从客户端组件中调用的函数。可以把它们看作更易编写的、完全类型化的 API 路由。
它们必须始终是一个 async 函数,并且在函数顶部用 "use server" 标记。
export default function Index() { return ( <Button title="按我" onPress={async () => { 'use server'; // 这段代码在服务器上运行。 console.log('Button pressed'); return '...'; }} /> ); }
你可以创建一个客户端组件来调用这个服务器函数:
'use client'; import { Text } from 'react-native'; export default function Button({ title, onPress }) { return <Text onPress={() => onPress()}>{title}</Text>; }
服务器函数也可以定义在一个独立文件中(在顶部带有 "use server"),然后从客户端组件中导入:
'use server'; export async function callAction() { // ... }
它们可以在客户端组件中这样使用:
import { Text } from 'react-native'; import { callAction } from './server-actions'; export default function Button({ title }) { return <Text onPress={() => callAction()}>{title}</Text>; }
重点
- 作为参数传给服务器函数时,你只能传递可序列化的数据。
- 服务器函数只能返回可序列化的数据。
- 服务器函数在服务器上运行,是放置那些不应暴露给客户端的逻辑的好地方。
- 服务器函数目前不能在 DOM 组件内部使用。
在服务器函数中渲染
Expo Router 中的 React 服务器函数可以在服务器上渲染 React 组件,并将一个 RSC 负载(由 React 团队维护的一种自定义 JSON 风格格式)流式返回,用于在客户端渲染。这类似于 Web 上的服务端渲染(SSR)。
例如,下面的服务器函数会渲染一些文本:
'use server'; // 可选:出于保险起见,导入 "server-only"。 import 'server-only'; import { View, Image, Text } from 'react-native'; export async function renderProfile({ username, accessToken, }: { username: string; accessToken: string; }) { // 注意:这里可以进行限流、GDPR 和其他服务端操作。 // 安全地从 API 获取一些数据。 const { name, image } = await fetch(`https://api.example.com/profile/${username}`, { headers: { Authorization: `Bearer ${accessToken}`, // 由于这段代码会运行在服务器上,请安全地使用机密环境变量。 // 这里不需要 EXPO_PUBLIC_ 前缀。 'X-Secret': process.env.SECRET, }, }).then(res => res.json()); // 渲染 return ( <View> <Image source={{ uri: image }} /> <Text>{name}</Text> </View> ); }
这个服务器函数可以从客户端组件中调用,内容将流式返回到客户端:
'use client'; import { useLocalSearchParams } from 'expo-router'; import * as React from 'react'; import { Text } from 'react-native'; import { renderProfile } from '@/components/server-actions'; // 在数据获取期间渲染的加载状态。 function Fallback() { return <Text>加载中...</Text>; } export default function Profile() { const { username } = useLocalSearchParams(); const { accessToken } = useCustomAuthProvider(); // 使用用户名和访问令牌调用服务器函数。 const profile = React.useMemo( () => renderProfile({ username, accessToken }), [username, accessToken] ); // 使用 React Suspense 和自定义加载状态异步渲染 profile。 return <React.Suspense fallback={<Fallback />}>{profile}</React.Suspense>; }
库兼容性
并非所有库都已针对 React Server Components 进行优化。你可以使用 "use client" 指令将文件标记为 Client Component,并在 Server Component 中使用它。这可以用来临时规避兼容性问题。
例如,考虑一个还没有附带 "use client" 指令的库 react-native-unoptimized。你可以通过创建一个模块并逐个重新导出这些模块来规避:
// 该指令将此模块设为客户端渲染。 'use client'; // 从库中重新导出这些导入。 export { One, Two, Three } from 'react-native-unoptimized';
避免使用 export * from '...',因为这会破坏 server 和 client 之间互操作的一些内部机制。
在 Server Components 中,带有 "use client" 的模块不能通过点语法访问。这意味着像 StyleSheet.create 或 Platform.OS 这样的操作在没有 react-native 包中进一步优化的情况下将无法在服务器上工作。
Suspense
你可以使用 React Suspense,在等待数据加载时从服务器流式返回部分 UI。
在下面的示例中,客户端会立即返回 加载中... 文本,而当 <MediumTask> 在一秒后完成渲染时,它会将文本替换为 中等任务完成!。<ExpensiveTask> 需要三秒才能加载,完成后会将文本替换为 耗时任务完成!。
import { Suspense } from 'react'; import { renderMediumTask, renderExpensiveTask } from '@/actions/tasks'; export default function App() { return <Suspense fallback={<Text>加载中...</Text>}>{renderTasks()}</Suspense>; }
'use server'; export async function renderTasks() { return ( <Suspense fallback={<Text>加载中...</Text>}> <> <MediumTask /> <Suspense fallback={<Text>加载中...</Text>}> <ExpensiveTask /> </Suspense> </> </Suspense> ); } async function MediumTask() { // 等待一秒后再解析。 await new Promise(resolve => setTimeout(resolve, 1000)); return <Text>中等任务完成!</Text>; } async function ExpensiveTask() { // 等待三秒后再解析。 await new Promise(resolve => setTimeout(resolve, 3000)); return <Text>耗时任务完成!</Text>; }
如果你移除 <ExpensiveTask> 外层的 Suspense 包裹,你会发现 加载中... 会等待这两个组件都渲染完成后才更新 UI。这使你能够逐步控制加载状态。有时候,一次性等待所有内容加载完成是有意义的(大多数情况下),而在其他时候,只要有内容就尽快流式返回 UI 会更有帮助(比如 ChatGPT 中的文本响应)。
密钥
Server Components 可以访问密钥和服务端 API。你可以使用 process.env 对象来访问环境变量。你可以在项目中导入 server-only 模块,以确保某个模块永远不会在客户端运行。
// 如果该模块在客户端运行,这里会抛出断言。 import 'server-only'; import { Text } from 'react-native'; export async function renderData() { // 这段代码只会在服务器上运行。 const data = await fetch('https://my-endpoint/', { headers: { Authorization: `Bearer ${process.env.SECRET}`, }, }); // ... return <div />; }
你可以在 .env 文件中定义密钥:
SECRET=123
你无需重启开发服务器即可更新环境变量。它们会在每次请求时自动重新加载。
平台检测
要检测你的代码被打包到哪个平台,请使用 process.env.EXPO_OS 环境变量。例如,process.env.EXPO_OS === 'ios'。相比 Platform.OS,更推荐使用它,因为 react-native 目前还没有为 React Server Components 做完全优化,并且可能无法按预期工作。
你可以通过执行 typeof window === 'undefined' 检查来判断代码是否在服务器上运行。这在客户端设备上总会返回 true,在服务器上返回 false。
使用 jest 进行测试
库作者可以使用 jest-expo 测试其模块是否支持服务器组件。有关更多信息,请参阅测试 React 服务器组件指南。
元数据
React Server Components 是 React 19 的一项特性。为了启用它们,Expo CLI 会在所有平台上自动使用 React 的特殊 canary 构建版本。未来,当 React Native 默认启用 React 19 时,它将被移除。
因此,你可以使用 React 19 的功能,例如在应用中的任意位置放置 <meta> 标签(仅限 web)。
export default function Index() { return ( <> {process.env.EXPO_OS === 'web' && ( <> <meta name="description" content="你好,世界!" /> <meta property="og:image" content="/og-image.png" /> </> )} <MyComponent /> </> ); }
你可以用它来替代 expo-router/head 中的 Head 组件,但目前它仅在 web 上可用。
请求头
你可以使用 expo-router/rsc/headers 模块访问用于向 Server Component 发起请求时所使用的请求头。
import { unstable_headers } from 'expo-router/rsc/headers'; export async function renderHome() { const authorization = (await unstable_headers()).get('authorization'); return <Text>{authorization}</Text>; }
unstable_headers 函数返回一个 promise,该 promise 会解析为一个只读的 Headers 对象。
要点
- 该 API 不能与构建时渲染(
render: 'static')一起使用,因为请求头会根据请求动态变化。未来,如果输出模式为static,该 API 将会抛出断言。 unstable_headers仅限服务器端使用,不能在客户端使用。
完整 React Server Components 模式
重要 该模式处于 实验阶段。
启用完整的 React Server Components 支持后,你可以利用更多功能。在此模式下,路由的默认渲染模式是 Server Components,而不是 Client Components。它仍在开发中,因为路由器和 React Navigation 需要重写以支持并发。
要启用完整的 Server Components 模式,你需要在 app 配置中启用 reactServerComponentRoutes 标志:
{ "expo": { "experiments": { "reactServerFunctions": true, "reactServerComponentRoutes": true } } }
启用后,所有路由默认都会以 Server Components 的形式渲染。未来,这将减少服务端/客户端之间的瀑布式请求,并启用构建时渲染以提供更好的离线支持。
- 目前没有栈式路由。自定义布局、
Stack、Tabs和Drawer目前都还不支持 Server Components。 - 大多数
Link组件属性目前也不支持。
重新加载 Server Components
这仅限于完整 React Server Components 模式。
Server Components 会在开发环境中于每次请求时重新加载。这意味着你可以修改服务器组件,并在客户端运行时中立即看到这些改动反映出来。你可能希望通过程序方式手动触发一次重新加载事件,以便重新获取数据或重新渲染组件。可以通过 useRouter 钩子中的 router.reload() 函数来实现。
'use client'; import { useRouter } from 'expo-router'; import { Text } from 'react-native'; export function Button() { const router = useRouter(); return ( <Text onPress={() => { // 重新加载当前路由。 router.reload(); }}> 重新加载当前路由 </Text> ); }
如果该路由是在构建时渲染的,那么它不会在客户端重新渲染。这是因为渲染代码并未包含在生产服务器中。
构建时渲染
这仅限于完整 React Server Components 模式。
Expo Router 支持两种不同的 Server Components 渲染模式:构建时渲染和请求时渲染。可以通过使用 unstable_settings 导出,在每个路由的基础上指定这些模式:
import { Text, View } from 'react-native'; export const unstable_settings = { // 该组件将在构建时渲染,并且在生产环境中永远不会重新渲染。 render: 'static', }; export default function Index() { return ( <View> <Text>你好,世界!</Text> </View> ); }
render: 'static'会在构建时渲染该组件,并且在生产环境中永远不会重新渲染它。这类似于传统静态站点生成器的工作方式。render: 'dynamic'会在请求时渲染该组件,并在每次请求时重新渲染它。这类似于服务端渲染的工作方式。
如果你想要客户端渲染,请将数据获取移动到 Client Component,并在本地控制渲染。
标记为 static 输出的路由会在构建时渲染,并嵌入到原生二进制文件中。这使得无需向服务器发起请求即可渲染路由(因为服务器请求已在应用被下载时完成)。
当前默认值是 dynamic 渲染。未来,我们会让缓存和优化变得更智能、更自动化。
你可以使用 generateStaticParams 函数在构建时生成静态页面。这对于只能在构建时运行而不能在服务器上运行的组件很有用。
import { Text } from 'react-native'; // 添加 `unstable_settings.render: 'static'` 将阻止此组件在服务器上运行。 export const unstable_settings = { render: 'static', }; // 该函数将为每种形状生成静态页面。 export async function generateStaticParams() { return [{ shape: 'square' }]; } export default function ShapeRoute({ shape }) { return <Text>{shape}</Text>; }
CSS
这仅限于完整 React Server Components 模式。
Expo Router 支持在 Server Components 中导入全局 CSS 和 CSS 模块。
import './styles.css'; import styles from './styles.module.css'; export default function Index() { return <div className={styles.container}>你好,世界!</div>; }
CSS 会从服务器提升到客户端 bundle 中。
部署
通用 React Server Components 仍处于 beta 阶段。
Web
首先,构建 Web 项目:
- npx expo export -p web然后你可以使用 npx expo serve 在本地托管它,或者将其部署到云端:
EAS Hosting 是部署你的 Expo API 路由和服务器的最佳方式。
原生
你可以按照服务器部署指南来部署你的原生 React Server Components:
将版本化服务器部署并关联到你的生产原生应用。
已知限制
这是一个非常早期的技术预览版,我们正在积极开发中。
- Expo Snack 不支持捆绑服务端组件。
- EAS Update 目前还不支持服务端组件。
- DOM 组件目前还不能在生产环境中使用 React 服务端函数。
- 生产部署受到限制,目前不建议使用。
- 目前不支持将 RSC 负载服务端渲染为 HTML。这意味着静态输出和服务端输出目前还不能完全正常工作。
generateStaticParams在完整的 React 服务端组件模式下仅部分受支持。- 目前不支持 HTML
form与服务端函数的集成(该功能会自动部分生效,但数据未加密)。 StyleSheet.create和Platform.OS在原生平台上不受支持。请对样式使用标准对象,并使用process.env.EXPO_OS进行平台检测。- 由于 Hermes 运行时的限制,调用其他服务端函数的 React 服务端函数在 Hermes 上不受支持。使用 Static Hermes 后可能会解决此问题。