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 中默认启用。

Prerequisites

2 requirements

1.

使用 Expo Router 的项目

如果你还没有,请查看 Expo Router 安装指南

2.

React Native 新架构

需要 React Native New Architecture,并且从 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,它将被流式传输到客户端。

服务端组件

服务端组件在服务端运行,这意味着它们可以访问服务端 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> ); }

要将其作为服务端组件渲染,你需要从一个服务端函数中返回它。

重点

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

客户端组件

由于 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 服务器函数

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

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

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

你可以创建一个客户端组件来调用这个服务器函数:

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

服务器函数也可以定义在一个独立文件中(在顶部带有 "use server"),然后从客户端组件中导入:

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

它们可以在客户端组件中这样使用:

components/button.tsx
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)。

例如,下面的服务器函数会渲染一些文本:

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> ); }

这个服务器函数可以从客户端组件中调用,内容将流式返回到客户端:

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>加载中...</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。你可以通过创建一个模块并逐个重新导出这些模块来规避:

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。

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

app/index.tsx (客户端组件)
import { Suspense } from 'react'; import { renderMediumTask, renderExpensiveTask } from '@/actions/tasks'; export default function App() { return <Suspense fallback={<Text>加载中...</Text>}>{renderTasks()}</Suspense>; }
actions/tasks.tsx (服务端函数)
'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 模块,以确保某个模块永远不会在客户端运行。

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 测试其模块是否支持服务器组件。有关更多信息,请参阅测试 React 服务器组件指南。

元数据

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

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> ); }

如果该路由是在构建时渲染的,那么它不会在客户端重新渲染。这是因为渲染代码并未包含在生产服务器中。

构建时渲染

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

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 中。

部署

Web

首先,构建 Web 项目:

Terminal
npx expo export -p web

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

使用 EAS 立即部署

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

原生

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

将原生服务器部署到 EAS

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

已知限制

  • Expo Snack 不支持捆绑服务端组件。
  • EAS Update 目前还不支持服务端组件。
  • DOM 组件目前还不能在生产环境中使用 React 服务端函数。
  • 生产部署受到限制,目前不建议使用。
  • 目前不支持将 RSC 负载服务端渲染为 HTML。这意味着静态输出和服务端输出目前还不能完全正常工作。
  • generateStaticParams 在完整的 React 服务端组件模式下仅部分受支持。
  • 目前不支持 HTML form 与服务端函数的集成(该功能会自动部分生效,但数据未加密)。
  • StyleSheet.createPlatform.OS 在原生平台上不受支持。请对样式使用标准对象,并使用 process.env.EXPO_OS 进行平台检测。
  • 由于 Hermes 运行时的限制,调用其他服务端函数的 React 服务端函数在 Hermes 上不受支持。使用 Static Hermes 后可能会解决此问题。