数据加载器
学习如何在 Expo Router 中使用数据加载器从服务器获取数据。
数据加载器使你的路由能够进行服务器端数据获取。通过从路由文件导出一个 loader 函数,你可以在服务器上获取数据,并使用 useLoaderData 钩子在组件中访问这些数据。这使你可以将敏感数据和 API 密钥保留在服务器上,同时为组件提供所需的数据。
🌐 Data loaders enable server-side data fetching for your routes. By exporting a loader function from a route file, you can fetch data on the server and access it in your component using the useLoaderData hook. This lets you keep sensitive data and API keys on the server while providing your components with the data they need.
设置
🌐 Setup
1
通过在项目的 应用配置 中向 expo-router 插件添加 unstable_useServerDataLoaders 选项来启用数据加载器:
🌐 Enable data loaders in your project's app config by adding the unstable_useServerDataLoaders option to the expo-router plugin:
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": [ [ "expo-router", { "unstable_useServerDataLoaders": true, "unstable_useServerRendering": true } ] ] } }
2
配置你的网页输出模式。数据加载器可以同时用于静态渲染 (web.output: 'static')和服务器渲染 (web.output: 'server'):
🌐 Configure your web output mode. Data loaders work with both static rendering (web.output: 'static') and server rendering (web.output: 'server'):
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "web": { %%placeholder-start%%... %%placeholder-end%% "output": "server" } } }
3
启动开发服务器:
🌐 Start the development server:
- npx expo start基本示例
🌐 Basic example
从你的路由文件中导出一个 loader 函数,并使用 useLoaderData 钩子在你的组件中访问数据:
🌐 Export a loader function from your route file and use the useLoaderData hook to access the data in your component:
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader() { // Fetch data from an API, database, or any server-side source const response = await fetch('https://api.example.com/data'); return response.json(); } export default function Home() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>Data: {JSON.stringify(data)}</Text> </View> ); }
loader 函数在服务器上执行,其返回值会被序列化并传递给你的组件。这意味着你可以安全地使用服务器端的密钥、数据库连接以及其他不应暴露给客户端的资源。在使用 TypeScript 时,将 typeof loader 作为泛型参数传递给 useLoaderData 可以让该 hook 从你的 loader 函数中推断返回类型。
🌐 The loader function executes on the server, and its return value is serialized and passed to your component. This means you can safely use server-side secrets, database connections, and other resources that should not be exposed to the client. When using TypeScript, passing typeof loader as the generic parameter to useLoaderData allows the hook to infer the return type from your loader function.
信息
useLoaderData钩子不需要在路由组件本身调用。它可以在路由组件树中的任何子组件中调用。
使用悬念
🌐 Using Suspense
当一个组件在数据仍在加载时调用 useLoaderData 钩子时,React 会暂停该组件。加载状态会沿组件树向上层级传递,直到到达最近的 <Suspense> 边界,然后该边界会渲染其备用内容。
🌐 When a component calls the useLoaderData hook while data is still loading, React suspends that component. The loading state cascades up the component tree until it reaches the nearest <Suspense> boundary, which then renders its fallback.
这可以让你通过在组件树中放置 <Suspense> 边界来精确控制加载备用内容出现的位置:
🌐 This lets you control exactly where loading fallbacks appear by placing <Suspense> boundaries in your component tree:
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>Welcome</Text> <Suspense fallback={<Text>Loading...</Text>}> <DataSection /> </Suspense> </View> ); } function DataSection() { const data = useLoaderData<typeof loader>(); return <Text>{data.title}</Text>; }
在上面的例子中,useLoaderData 位于 <Home> 的子组件中,并用 <Suspense> 封装以显示加载状态。
🌐 In the above example, useLoaderData is in a child component of <Home> and wrapped it with <Suspense> to show a loading state.
错误处理
🌐 Error handling
当加载器抛出错误时,该错误会传播到最近的错误边界。你可以从同一路由文件中导出一个ErrorBoundary组件来处理加载器错误:
🌐 When a loader throws an error, it propagates to the nearest error boundary. You can export an ErrorBoundary component from the same route file to handle loader errors:
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('Failed to fetch data'); } return response.json(); } export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) { return ( <View> <Text>Error: {error.message}</Text> <Text onPress={retry}>Try again</Text> </View> ); } export default function DataPage() { const data = useLoaderData<typeof loader>(); return ( <View> <Text>{data.title}</Text> </View> ); }
当没有导出 ErrorBoundary 时,错误会传播到最近的父路由的错误边界。你也可以在路由中使用自定义错误边界组件来在组件树的特定位置捕获错误。
🌐 When no ErrorBoundary is exported, the error propagates to the nearest parent route's error boundary. You can also use custom error boundary components within your route to catch errors at specific points in the component tree.
动态路由
🌐 Dynamic routes
加载器将路由参数作为第二个参数接收:
🌐 Loaders receive route parameters as the second argument:
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> ); }
正在访问请求
🌐 Accessing the request
警告 在使用静态渲染时,
request参数为undefined,因为在构建时没有 HTTP 请求。
在使用 服务器渲染 时,加载器会将传入的 HTTP 请求作为第一个参数接收。这使你可以访问头信息、cookie 和其他请求信息:
🌐 When using server rendering, loaders receive the incoming HTTP request as the first argument. This allows you to access headers, cookies, and other request information:
import { Text, View } from 'react-native'; import { useLoaderData } from 'expo-router'; export async function loader(request) { // Access authorization header const authToken = request?.headers.get('Authorization'); if (!authToken) { return { user: null }; } // Fetch user data using the token 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>Please log in</Text>; } return ( <View> <Text>Welcome, {user.name}</Text> </View> ); }
返回数据
🌐 Returning data
加载器可以将数据作为纯 JSON 返回,这些数据可以使用 JSON.parse 轻松反序列化。这包括对象、数组或任何其他可以用 JSON.stringify 序列化的原始类型。
🌐 Loaders can return data as plain JSON, which is easily deserialized using JSON.parse. This includes objects, arrays or any other primitive that can be serialized with JSON.stringify.
export async function loader() { const response = await fetch('https://api.example.com/data'); return response.json(); }
如果你的加载器返回 undefined 或 null,该值会被标准化为 null。
🌐 If your loader returns undefined or null, the value is normalized to null.
运行时 API
🌐 Runtime API
数据加载器可以完全访问 expo-server 提供的 Runtime API。这包括用于设置响应头、抛出 HTTP 错误以及运行后台任务的工具:
🌐 Data loaders have full access to the Runtime API from expo-server. This includes utilities for setting response headers, throwing HTTP errors, and running background tasks:
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 文档。
🌐 See the Runtime API documentation for a full list of available functions.
环境变量
🌐 Environment variables
加载器在服务器上运行,并可以访问 process.env。加载器使用的环境变量永远不会暴露给客户端包。这对于访问 API 密钥和其他秘密信息非常有用:
🌐 Loaders run on the server and have access to process.env. Environment variables used in loaders are never exposed to the client bundle. This is useful for accessing API keys and other secrets:
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> ); }
静态渲染和服务器渲染的区别
🌐 Difference between static and server rendering
数据加载器的行为会根据你的 web.output 配置而有所不同:
🌐 Data loaders behave differently depending on your web.output configuration:
| 方面 | 静态渲染 | 服务器渲染 |
|---|---|---|
| 加载器执行 | 构建时 | 请求时 |
request 参数 | undefined | ImmutableRequest |
| 最适合 | 博客、营销页面、文档 | 个性化内容、依赖身份验证的页面 |
静态渲染
🌐 Static rendering
使用静态渲染时,加载器会在使用 npx expo export 导出应用时执行。数据会嵌入生成的 HTML 和 JSON 文件中。这意味着:
🌐 With static rendering, loaders execute during when exporting your app with npx expo export. The data is embedded in the generated HTML and JSON files. This means:
- 数据在构建时确定,并且在下次构建之前不会更改
request参数是undefined因为在构建过程中没有 HTTP 请求- 适合不经常更改的内容
服务器渲染
🌐 Server rendering
使用服务器渲染时,加载器会在每次请求时执行。这意味着:
🌐 With server rendering, loaders execute on every request. This means:
request参数包含传入 HTTP 请求的不可变版本- 生产部署需要
expo-server
类型化加载器函数
🌐 Typed loader functions
为了提高类型安全性,你可以从 expo-router 导入 LoaderFunction 类型:
🌐 For improved type safety, you can import the LoaderFunction type from expo-router:
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> ); }
已知的限制
🌐 Known limitations
- 加载器必须返回可 JSON 序列化的数据。目前不支持流式响应。这将在未来版本中解决。
- 在导航过程中,加载器数据会缓存在客户端。目前没有内置的方法来清除此缓存。这个问题将在未来的版本中解决。
常见问题
🌐 Common questions
我可以在不使用服务器渲染的情况下使用数据加载器吗?
是的。数据加载器可以用于静态渲染(web.output: 'static')和服务器渲染(web.output: 'server')。
🌐 Yes. Data loaders work with both static rendering (web.output: 'static') and server rendering (web.output: 'server').
客户端包中包含加载器吗?
不,loader 的导出会从客户端包中移除。然而,如果另一个模块包含服务器端逻辑,并被位于 app 目录之外的客户端代码导入,它可能会被包含在你的客户端包中。
🌐 No, loader exports are dropped from the client bundle. However, if another module contains server-side logic and is imported by client-side code outside of the app directory, it may be included in your client-side bundle.