在 Expo Router 应用中使用 React Server Components

编辑页面

了解在 Expo 中于服务器端渲染 React 组件。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

实验性 可用,适用于 SDK 52 及更高版本。这是一个 beta 版本,可能会发生破坏性变更。

React Server Components 支持许多令人兴奋的能力,包括:

  • 使用异步组件和 React Suspense 进行数据获取。
  • 使用密钥和服务端 API。
  • 用于 SEO 和性能的服务端渲染(SSR)。
  • 构建时渲染,以移除未使用的 JS 代码。

Expo Router 在所有平台上都支持 React Server Components。这是该功能的早期 预览,未来将会在 Expo Router 中默认启用。

前置条件

你的项目必须使用 Expo Router 和 React Native 新架构(从 SDK 52 开始默认启用)。

用法

要在 Expo 应用中使用 React Server Components,你需要:

  1. 安装所需的 RSC 依赖

    Terminal
    npx expo install react-server-dom-webpack
  2. 确保入口模块在 package.json 中是 expo-router/entry(默认)。

  3. 在项目 app 配置中启用该标志:

app.json
{ "expo": { "experiments": { "reactServerFunctions": true } } }
  1. 确保你的 app 配置中的任何位置都没有将 "origin" 设置为布尔值。
  2. 创建一个初始路由 app/index.tsx
app/index.tsx (Client Component)
/// <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> ); }
  1. 创建一个 Server Function actions/render-info.tsx
actions/render-info.tsx (Server Function)
'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。更多输出模式的支持即将推出。

Server Components

Server Components 在服务端运行,这意味着它们可以访问服务端 API 和 Node.js 内置模块(在本地运行时)。它们也可以使用异步组件。

考虑下面这个会获取数据并渲染它的组件:

components/pokemon.tsx
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> ); }

要将其作为 server component 渲染,你需要从一个 Server Function 中返回它。

重点

  • 你不能在 Server Components 中使用 useStateuseEffectuseContext 之类的 hooks。
  • 你不能在 Server Components 中使用浏览器或原生 API。
  • "use server" 并不意味着将文件标记为 server component。它用于标记文件中导出了 React Server Functions。
  • Server components 可以访问所有环境变量,因为它们在客户端之外安全运行。

Client Components

由于 Server Components 不能访问原生 API 或 React Context,你可以创建一个 Client Component 来使用这些特性。它们通过在文件顶部添加 "use client" 指令来创建。

components/button.tsx
'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 Server Functions

Server Functions 是在服务器上运行,并且可以从 Client Components 中调用的函数。可以把它们看作更易编写的、完全类型化的 API 路由。

它们必须始终是一个 async 函数,并且在函数顶部用 "use server" 标记。

app/index.tsx
export default function Index() { return ( <Button title="Press me" onPress={async () => { 'use server'; // 这段代码在服务器上运行。 console.log('Button pressed'); return '...'; }} /> ); }

你可以创建一个 Client Component 来调用这个 Server Function:

components/button.tsx
'use client'; import { Text } from 'react-native'; export default function Button({ title, onPress }) { return <Text onPress={() => onPress()}>{title}</Text>; }

Server Functions 也可以定义在一个独立文件中(在顶部带有 "use server"),然后从 Client Components 中导入:

components/server-actions.tsx
'use server'; export async function callAction() { // ... }

它们可以在 Client Component 中这样使用:

components/button.tsx
import { Text } from 'react-native'; import { callAction } from './server-actions'; export default function Button({ title }) { return <Text onPress={() => callAction()}>{title}</Text>; }

重点

  • 作为参数传给 Server Functions 时,你只能传递可序列化的数据。
  • Server Functions 只能返回可序列化的数据。
  • Server Functions 在服务器上运行,是放置那些不应暴露给客户端的逻辑的好地方。
  • Server Functions 目前不能在 DOM 组件内部使用

在 Server Functions 中渲染

Expo Router 中的 React Server Functions 可以在服务器上渲染 React 组件,并将一个 RSC payload(由 React 团队维护的一种自定义 JSON 风格格式)流式返回,用于在客户端渲染。这类似于 Web 上的服务端渲染(SSR)。

例如,下面的 Server Function 会渲染一些文本:

components/server-actions.tsx
'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> ); }

这个 Server Function 可以从 Client Component 中调用,内容将流式返回到客户端:

components/profile.tsx
'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>Loading...</Text>; } export default function Profile() { const { username } = useLocalSearchParams(); const { accessToken } = useCustomAuthProvider(); // 使用用户名和访问令牌调用 Server Function。 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。你可以通过创建一个模块并逐个重新导出这些模块来规避:

lib/react-native-unoptimized.tsx
// 该指令将此模块设为客户端渲染。 'use client'; // 从库中重新导出这些导入。 export { One, Two, Three } from 'react-native-unoptimized';

避免使用 export * from '...',因为这会破坏 server 和 client 之间互操作的一些内部机制。

在 Server Components 中,带有 "use client" 的模块不能通过点语法访问。这意味着像 StyleSheet.createPlatform.OS 这样的操作在没有 react-native 包中进一步优化的情况下将无法在服务器上工作。

Suspense

你可以使用 React Suspense,在等待数据加载时从服务器流式返回部分 UI。

在下面的示例中,客户端会立即返回 Loading... 文本,而当 <MediumTask> 在一秒后完成渲染时,它会将文本替换为 Medium task done!<ExpensiveTask> 需要三秒才能加载,完成后会将文本替换为 Expensive task done!

app/index.tsx (Client Component)
import { Suspense } from 'react'; import { renderMediumTask, renderExpensiveTask } from '@/actions/tasks'; export default function App() { return <Suspense fallback={<Text>Loading...</Text>}>{renderTasks()}</Suspense>; }
actions/tasks.tsx (Server Functions)
'use server'; export async function renderTasks() { return ( <Suspense fallback={<Text>Loading...</Text>}> <> <MediumTask /> <Suspense fallback={<Text>Loading...</Text>}> <ExpensiveTask /> </Suspense> </> </Suspense> ); } async function MediumTask() { // 等待一秒后再解析。 await new Promise(resolve => setTimeout(resolve, 1000)); return <Text>Medium task done!</Text>; } async function ExpensiveTask() { // 等待三秒后再解析。 await new Promise(resolve => setTimeout(resolve, 3000)); return <Text>Expensive task done!</Text>; }

如果你移除 <ExpensiveTask> 外层的 Suspense 包裹,你会发现 Loading... 会等待这两个组件都渲染完成后才更新 UI。这使你能够逐步控制加载状态。有时候,一次性等待所有内容加载完成是有意义的(大多数情况下),而在其他时候,只要有内容就尽快流式返回 UI 会更有帮助(比如 ChatGPT 中的文本响应)。

密钥

Server Components 可以访问密钥和服务端 API。你可以使用 process.env 对象来访问环境变量。你可以在项目中导入 server-only 模块,以确保某个模块永远不会在客户端运行。

actions/renderData.tsx
// 如果该模块在客户端运行,这里会抛出断言。 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 文件中定义密钥:

.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 测试他们的模块是否支持 Server Components。更多信息请参阅 Testing React Server Components 指南。

元数据

React Server Components 是 React 19 的一项特性。为了启用它们,Expo CLI 会在所有平台上自动使用 React 的特殊 canary 构建版本。未来,当 React Native 默认启用 React 19 时,它将被移除。

因此,你可以使用 React 19 的功能,例如在应用中的任意位置放置 <meta> 标签(仅限 web)。

app/index.tsx
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 发起请求时所使用的请求头。

actions/renderHome.tsx
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 标志:

app.json
{ "expo": { "experiments": { "reactServerFunctions": true, "reactServerComponentRoutes": true } } }

启用后,所有路由默认都会以 Server Components 的形式渲染。未来,这将减少服务端/客户端之间的瀑布式请求,并启用构建时渲染以提供更好的离线支持。

  • 目前没有栈式路由。自定义布局、StackTabsDrawer 目前都还不支持 Server Components。
  • 大多数 Link 组件属性目前也不支持。

重新加载 Server Components

这仅限于完整 React Server Components 模式。

Server Components 会在开发环境中于每次请求时重新加载。这意味着你可以修改服务器组件,并在客户端运行时中立即看到这些改动反映出来。你可能希望通过程序方式手动触发一次重新加载事件,以便重新获取数据或重新渲染组件。可以通过 useRouter 钩子中的 router.reload() 函数来实现。

components/button.tsx
'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 导出,在每个路由的基础上指定这些模式:

app/index.tsx
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 函数在构建时生成静态页面。这对于只能在构建时运行而不能在服务器上运行的组件很有用。

app/shapes/[shape].tsx
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 模块。

app/index.tsx
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 项目:

Terminal
npx expo export -p web

然后你可以使用 npx expo serve 在本地托管它,或者将其部署到云端:

使用 EAS 立即部署

EAS Hosting 是部署你的 Expo API 路由和服务器的最佳方式。

原生

你可以按照服务器部署指南来部署你的原生 React Server Components:

将原生服务器部署到 EAS

将版本化服务器部署并关联到你的生产原生应用。

已知限制

这是一个非常早期的技术预览版,我们正在积极开发中。

  • Expo Snack 不支持打包 Server Components。
  • EAS Update 目前还不能与 Server Components 一起使用。
  • DOM 组件目前还不能在生产环境中使用 React Server Functions。
  • 生产部署的限制较多,目前还不推荐使用。
  • 将 RSC payload 进行服务端渲染并输出为 HTML 目前还不支持。这意味着静态输出和服务器输出目前都还不能完全正常工作。
  • generateStaticParams 在完整 React Server Components 模式下仅获得部分支持。
  • HTML form 与 Server Functions 的集成目前不支持(虽然这部分会自动工作,但数据不会被加密)。
  • StyleSheet.createPlatform.OS 在原生平台上不受支持。样式请使用标准对象,平台检测请使用 process.env.EXPO_OS
  • 在 Hermes 运行时存在限制,因此在 Hermes 上不支持调用其他 Server Functions 的 React Server Functions。这个问题可能会通过 Static Hermes 得到解决。