数据加载器

编辑页面

了解如何使用 Expo Router 中的数据加载器在服务器上获取数据。


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

数据加载器处于 alpha 阶段,并且可在 SDK 55 及更高版本中使用。它们需要 静态渲染服务器渲染

数据加载器为你的路由启用服务端数据获取。通过从路由文件中导出 loader 函数,你可以在服务器上获取数据,并使用 useLoaderData 钩子在组件中访问这些数据。这让你能够将敏感数据和 API 密钥保留在服务器上,同时为组件提供所需的数据。

设置

1

通过向 expo-router 插件添加 unstable_useServerDataLoaders 选项,在项目的 app 配置 中启用数据加载器:

app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": [ [ "expo-router", { "unstable_useServerDataLoaders": true, "unstable_useServerRendering": true } ] ] } }

2

配置你的 web 输出模式。数据加载器同时适用于 静态渲染web.output: 'static')和 服务器渲染web.output: 'server'):

app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { %%placeholder-start%%... %%placeholder-end%% "output": "server" } } }

3

启动开发服务器:

Terminal
npx expo start

基本示例

从你的路由文件中导出 loader 函数,并使用 useLoaderData 钩子在组件中访问数据:

src/app/index.tsx
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader() { // 从 API、数据库或任何服务端数据源获取数据 const response = await fetch('https://api.example.com/data'); return response.json(); } export default function Home() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>数据:{JSON.stringify(data)}</Text> </View> ); }

loader 函数在服务器上执行,其返回值会被序列化并传递给你的组件。这意味着你可以安全地使用服务端密钥、数据库连接以及其他不应暴露给客户端的资源。在使用 TypeScript 时,将 typeof loader 作为 useLoaderData 的泛型参数,可以让该钩子从你的 loader 函数中推断返回类型。

useLoaderData 钩子不需要在路由组件本身中调用。它可以在路由组件树中的任何子组件里调用。

使用 Suspense

当组件调用 useLoaderData 钩子时,如果数据仍在加载中,React 会挂起该组件。加载状态会沿着组件树向上传播,直到到达最近的 <Suspense> 边界,然后渲染其回退内容。

这让你可以通过在组件树中放置 <Suspense> 边界,精确控制加载回退内容出现的位置:

src/app/index.tsx
import { Suspense } from 'react'; import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader() { const response = await fetch('https://api.example.com/data'); return response.json(); } export default function Home() { return ( <View> <Text>欢迎</Text> <Suspense fallback={<Text>加载中...</Text>}> <DataSection /> </Suspense> </View> ); } function DataSection() { const data = useLoaderData<typeof loader>(); return <Text>{data.title}</Text>; }

在上面的示例中,useLoaderData 位于 <Home> 的子组件中,并由 <Suspense> 包裹以显示加载状态。

错误处理

当 loader 抛出错误时,它会传播到最近的 错误边界。你可以从同一个路由文件中导出一个 ErrorBoundary 组件来处理 loader 错误:

src/app/data.tsx
import { Text, View } from 'react-native'; import { useLoaderData, type ErrorBoundaryProps } from 'expo-router'; export async function loader() { const response = await fetch('https://api.example.com/data'); if (!response.ok) { throw new Error('获取数据失败'); } return response.json(); } export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) { return ( <View> <Text>错误:{error.message}</Text> <Text onPress={retry}>再试一次</Text> </View> ); } export default function DataPage() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>{data.title}</Text> </View> ); }

当没有导出 ErrorBoundary 时,错误会传播到最近的父路由错误边界。你也可以在路由内部使用自定义错误边界组件,在组件树的特定位置捕获错误。

动态路由

loader 会将路由参数作为第二个参数接收:

src/app/posts/[postId].tsx
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader(request, params) { const response = await fetch(`https://api.example.com/posts/${params.postId}`); return response.json(); } export default function Post() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>{data.title}</Text> <Text>{data.content}</Text> </View> ); }

访问请求

在使用静态渲染时,request 参数为 undefined,因为在构建时没有 HTTP 请求。

在使用 服务器渲染 时,loader 会将传入的 HTTP 请求作为第一个参数接收。这使你能够访问请求头、cookie 和其他请求信息:

src/app/profile.tsx
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader(request) { // 访问授权头 const authToken = request?.headers.get('Authorization'); if (!authToken) { return { user: null }; } // 使用令牌获取用户数据 const response = await fetch('https://api.example.com/user', { headers: { Authorization: authToken }, }); return { user: await response.json() }; } export default function Profile() { const { user } = useLoaderData<typeof loader>(); if (!user) { return <Text>请登录</Text>; } return ( <View> <Text>欢迎,{user.name}</Text> </View> ); }

返回数据

loader 可以将数据作为普通 JSON 返回,这可通过 JSON.parse 轻松反序列化。这包括对象、数组或任何其他可通过 JSON.stringify 序列化的原始值。

src/app/index.tsx
export async function loader() { const response = await fetch('https://api.example.com/data'); return response.json(); }

如果你的 loader 返回 undefinednull,该值会被规范化为 null

运行时 API

数据加载器可以完整访问来自 expo-server运行时 API。这包括用于设置响应头、抛出 HTTP 错误以及运行后台任务的工具:

src/app/example.tsx
import { setResponseHeaders, StatusError } from 'expo-server'; export async function loader(request) { const authToken = request?.headers.get('Authorization'); if (!authToken) { throw new StatusError(401, 'Unauthorized'); } setResponseHeaders({ 'Cache-Control': 'private, max-age=60' }); return { user: 'authenticated' }; }

有关可用函数的完整列表,请参阅 运行时 API 文档

环境变量

loader 在服务器上运行,并且可以访问 process.env。loader 中使用的环境变量绝不会暴露给客户端 bundle。这对于访问 API 密钥和其他机密非常有用:

src/app/api-data.tsx
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader() { const apiKey = process.env.API_SECRET_KEY; const response = await fetch('https://api.example.com/data', { headers: { 'X-API-Key': apiKey }, }); return response.json(); } export default function ApiData() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>{JSON.stringify(data)}</Text> </View> ); }

静态渲染与服务器渲染的区别

数据加载器的行为会根据你的 web.output 配置而有所不同:

方面静态渲染服务器渲染
loader 执行时机构建时请求时
request 参数undefinedImmutableRequest
最适合博客、营销页面、文档个性化内容、依赖身份验证的页面

静态渲染

在静态渲染下,loader 会在你使用 npx expo export 导出应用时执行。数据会嵌入生成的 HTML 和 JSON 文件中。这意味着:

  • 数据在构建时确定,在下一次构建之前不会改变
  • request 参数为 undefined,因为构建期间没有 HTTP 请求
  • 适合不经常变化的内容

服务器渲染

在服务器渲染下,loader 会在每次请求时执行。这意味着:

  • request 参数包含传入 HTTP 请求的不可变版本
  • 生产部署需要 expo-server

类型化的 loader 函数

为了提高类型安全性,你可以从 expo-router 导入 LoaderFunction 类型:

src/app/posts/[postId].tsx
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; import { type LoaderFunction } from 'expo-router/server'; type PostData = { title: string; content: string; }; export const loader: LoaderFunction<PostData> = async (request, params) => { const response = await fetch(`https://api.example.com/posts/${params.postId}`); return response.json(); }; export default function Post() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>{data.title}</Text> <Text>{data.content}</Text> </View> ); }

已知限制

  • Loader 必须 返回可序列化为 JSON 的数据。目前不支持流式响应。这个问题将在未来版本中解决。
  • Loader 数据会在导航期间缓存在客户端。目前没有内置方式来使此缓存失效。这个问题将在未来版本中解决。

常见问题

我可以在没有服务端渲染的情况下使用数据加载器吗?

可以。数据加载器既适用于静态渲染(web.output: 'static'),也适用于服务端渲染(web.output: 'server')。

加载器会包含在客户端包中吗?

不会,loader 导出会从客户端包中移除。不过,如果另一个模块包含服务端逻辑,并且被 src/app 目录之外的客户端代码导入,它可能会被包含在你的客户端包中。