模式和最佳实践
有一些在 React 和 Next.js 中获取数据的推荐模式和最佳实践。本页将介绍一些最常见的模式以及如何使用它们。
¥There are a few recommended patterns and best practices for fetching data in React and Next.js. This page will go over some of the most common patterns and how to use them.
在服务器上获取数据
¥Fetching data on the server
只要有可能,我们建议使用服务器组件在服务器上获取数据。这使你能够:
¥Whenever possible, we recommend fetching data on the server with Server Components. This allows you to:
-
可以直接访问后端数据资源(例如数据库)。
¥Have direct access to backend data resources (e.g. databases).
-
通过防止敏感信息(例如访问令牌和 API 密钥)暴露给客户端,使你的应用更加安全。
¥Keep your application more secure by preventing sensitive information, such as access tokens and API keys, from being exposed to the client.
-
在同一环境中获取数据并渲染。这减少了客户端和服务器之间的来回通信以及客户端上的 在主线程上工作。
¥Fetch data and render in the same environment. This reduces both the back-and-forth communication between client and server, as well as the work on the main thread on the client.
-
通过单次往返执行多次数据提取,而不是在客户端上执行多个单独的请求。
¥Perform multiple data fetches with single round-trip instead of multiple individual requests on the client.
-
减少客户端-服务器 waterfalls。
¥Reduce client-server waterfalls.
-
根据你所在的区域,数据提取也可以在靠近数据源的地方进行,从而减少延迟并提高性能。
¥Depending on your region, data fetching can also happen closer to your data source, reducing latency and improving performance.
然后,你可以使用 服务器操作 更改或更新数据。
¥Then, you can mutate or update data with Server Actions.
在需要的地方获取数据
¥Fetching data where it's needed
如果你需要在树中的多个组件中使用相同的数据(例如当前用户),则不必全局获取数据,也不必在组件之间转发 props。相反,你可以在需要数据的组件中使用 fetch
或 React cache
,而不必担心对同一数据发出多个请求的性能影响。
¥If you need to use the same data (e.g. current user) in multiple components in a tree, you do not have to fetch data globally, nor forward props between components. Instead, you can use fetch
or React cache
in the component that needs the data without worrying about the performance implications of making multiple requests for the same data.
这是可能的,因为 fetch
请求会自动记忆。了解有关 请求记忆 的更多信息
¥This is possible because fetch
requests are automatically memoized. Learn more about request memoization
很高兴知道:这也适用于布局,因为无法在父布局与其子布局之间传递数据。
¥Good to know: This also applies to layouts, since it's not possible to pass data between a parent layout and its children.
流式
¥Streaming
Streaming 和 悬念 是 React 功能,允许你逐步渲染 UI 的渲染单元并将其增量流式传输到客户端。
¥Streaming and Suspense are React features that allow you to progressively render and incrementally stream rendered units of the UI to the client.
使用服务器组件和 嵌套布局,你可以立即渲染页面中不特别需要数据的部分,并为正在获取数据的页面部分显示 加载状态。这意味着用户不必等待整个页面加载即可开始与其交互。
¥With Server Components and nested layouts, you're able to instantly render parts of the page that do not specifically require data, and show a loading state for parts of the page that are fetching data. This means the user does not have to wait for the entire page to load before they can start interacting with it.
要了解有关 Streaming 和 Suspense 的更多信息,请参阅 加载用户界面 和 流式和悬念 页面。
¥To learn more about Streaming and Suspense, see the Loading UI and Streaming and Suspense pages.
并行和顺序数据获取
¥Parallel and sequential data fetching
在 React 组件中获取数据时,你需要注意两种数据获取模式:并行和顺序。
¥When fetching data inside React components, you need to be aware of two data fetching patterns: Parallel and Sequential.
-
通过顺序数据获取,路由中的请求相互依赖,因此会产生瀑布。在某些情况下,你可能需要这种模式,因为一次提取取决于另一次提取的结果,或者你希望在下一次提取之前满足某个条件以节省资源。然而,这种行为也可能是无意的,并导致加载时间更长。
¥With sequential data fetching, requests in a route are dependent on each other and therefore create waterfalls. There may be cases where you want this pattern because one fetch depends on the result of the other, or you want a condition to be satisfied before the next fetch to save resources. However, this behavior can also be unintentional and lead to longer loading times.
-
通过并行数据获取,路由中的请求会被预先地发起并同时加载数据。这减少了客户端-服务器瀑布和加载数据所需的总时间。
¥With parallel data fetching, requests in a route are eagerly initiated and will load data at the same time. This reduces client-server waterfalls and the total time it takes to load data.
顺序数据获取
¥Sequential Data Fetching
如果你有嵌套组件,并且每个组件都获取自己的数据,那么如果这些数据请求不同,则数据获取将按顺序发生(这不适用于对相同数据的请求,因为它们会自动 memoized)。
¥If you have nested components, and each component fetches its own data, then data fetching will happen sequentially if those data requests are different (this doesn't apply to requests for the same data as they are automatically memoized).
例如,只有当 Artist
组件完成获取数据后,Playlists
组件才会开始获取数据,因为 Playlists
依赖于 artistID
属性:
¥For example, the Playlists
component will only start fetching data once the Artist
component has finished fetching data because Playlists
depends on the artistID
prop:
// ...
async function Playlists({ artistID }: { artistID: string }) {
// Wait for the playlists
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// Wait for the artist
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
// ...
async function Playlists({ artistID }) {
// Wait for the playlists
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({ params: { username } }) {
// Wait for the artist
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
在这种情况下,你可以使用 loading.js
(对于路由段)或 React <Suspense>
(对于嵌套组件)来显示即时加载状态,同时 React 流式传输结果。
¥In cases like this, you can use loading.js
(for route segments) or React <Suspense>
(for nested components) to show an instant loading state while React streams in the result.
这将防止整个路由被数据获取阻塞,并且用户将能够与页面中未被阻塞的部分进行交互。
¥This will prevent the whole route from being blocked by data fetching, and the user will be able to interact with the parts of the page that are not blocked.
阻止数据请求:
¥Blocking Data Requests:
防止瀑布的另一种方法是在应用的根部全局获取数据,但这将阻止其下方所有路由段的渲染,直到数据完成加载。这可以描述为 "全有或全无" 数据获取。你要么拥有页面或应用的完整数据,要么没有。
¥An alternative approach to prevent waterfalls is to fetch data globally, at the root of your application, but this will block rendering for all route segments beneath it until the data has finished loading. This can be described as "all or nothing" data fetching. Either you have the entire data for your page or application, or none.
任何带有
await
的获取请求都将阻止其下方整个树的渲染和数据获取,除非它们被封装在<Suspense>
边界中或使用loading.js
。另一种选择是使用 并行数据获取 或 预载模式。¥Any fetch requests with
await
will block rendering and data fetching for the entire tree beneath it, unless they are wrapped in a<Suspense>
boundary orloading.js
is used. Another alternative is to use parallel data fetching or the preload pattern.
并行数据获取
¥Parallel Data Fetching
要并行获取数据,你可以通过在使用数据的组件外部定义请求,然后从组件内部调用它们来快速发起请求。通过并行启动两个请求可以节省时间,但是,在两个 promise 都得到解决之前,用户不会看到渲染的结果。
¥To fetch data in parallel, you can eagerly initiate requests by defining them outside the components that use the data, then calling them from inside the component. This saves time by initiating both requests in parallel, however, the user won't see the rendered result until both promises are resolved.
在下面的示例中,getArtist
和 getArtistAlbums
函数在 Page
组件外部定义,然后在组件内部调用,我们等待两个 Promise 解析:
¥In the example below, the getArtist
and getArtistAlbums
functions are defined outside the Page
component, then called inside the component, and we wait for both promises to resolve:
import Albums from './albums'
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getArtistAlbums(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// Initiate both requests in parallel
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)
// Wait for the promises to resolve
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getArtistAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params: { username } }) {
// Initiate both requests in parallel
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)
// Wait for the promises to resolve
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
为了提高用户体验,你可以添加 悬念边界 来分解渲染工作并尽快显示部分结果。
¥To improve the user experience, you can add a Suspense Boundary to break up the rendering work and show part of the result as soon as possible.
预加载数据
¥Preloading Data
防止瀑布的另一种方法是使用预加载模式。你可以选择创建 preload
函数来进一步优化并行数据获取。通过这种方法,你不必将 Promise 作为 props 传递下去。preload
函数也可以有任何名称,因为它是一个模式,而不是 API。
¥Another way to prevent waterfalls is to use the preload pattern. You can optionally create a preload
function to further optimize parallel data fetching. With this approach, you don't have to pass promises down as props. The preload
function can also have any name as it's a pattern, not an API.
import { getItem } from '@/utils/get-item'
export const preload = (id: string) => {
// void evaluates the given expression and returns undefined
// https://web.nodejs.cn/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
import { getItem } from '@/utils/get-item'
export const preload = (id) => {
// void evaluates the given expression and returns undefined
// https://web.nodejs.cn/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export default async function Item({ id }) {
const result = await getItem(id)
// ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({ params: { id } }) {
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
使用 React cache
、server-only
和预加载模式
¥Using React cache
, server-only
, and the Preload Pattern
你可以结合 cache
函数、preload
模式和 server-only
包来创建可在整个应用中使用的数据获取实用程序。
¥You can combine the cache
function, the preload
pattern, and the server-only
package to create a data fetching utility that can be used throughout your app.
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
import { cache } from 'react'
import 'server-only'
export const preload = (id) => {
void getItem(id)
}
export const getItem = cache(async (id) => {
// ...
})
通过这种方法,你可以预获取数据,缓存响应,并保证此数据获取 只发生在服务器上。
¥With this approach, you can eagerly fetch data, cache responses, and guarantee that this data fetching only happens on the server.
布局、页面或其他组件可以使用 utils/get-item
导出来控制何时获取项目的数据。
¥The utils/get-item
exports can be used by Layouts, Pages, or other components to give them control over when an item's data is fetched.
很高兴知道:
¥Good to know:
我们建议使用
server-only
包 以确保服务器数据获取功能永远不会在客户端上使用。¥We recommend using the
server-only
package to make sure server data fetching functions are never used on the client.
防止敏感数据暴露给客户端
¥Preventing sensitive data from being exposed to the client
我们建议使用 React 的 taint API taintObjectReference
和 taintUniqueValue
,以防止整个对象实例或敏感值传递到客户端。
¥We recommend using React's taint APIs, taintObjectReference
and taintUniqueValue
, to prevent whole object instances or sensitive values from being passed to the client.
要在应用中启用污染,请将 Next.js Config experimental.taint
选项设置为 true
:
¥To enable tainting in your application, set the Next.js Config experimental.taint
option to true
:
module.exports = {
experimental: {
taint: true,
},
}
然后将要污染的对象或值传递给 experimental_taintObjectReference
或 experimental_taintUniqueValue
函数:
¥Then pass the object or value you want to taint to the experimental_taintObjectReference
or experimental_taintUniqueValue
functions:
import { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'Do not pass the whole user object to the client',
data
)
experimental_taintUniqueValue(
"Do not pass the user's address to the client",
data,
data.address
)
return data
}
import { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'Do not pass the whole user object to the client',
data
)
experimental_taintUniqueValue(
"Do not pass the user's address to the client",
data,
data.address
)
return data
}
import { getUserData } from './data'
export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // this will cause an error because of taintObjectReference
address={userData.address} // this will cause an error because of taintUniqueValue
/>
)
}
import { getUserData } from './data'
export async function Page() {
const userData = await getUserData()
return (
<ClientComponent
user={userData} // this will cause an error because of taintObjectReference
address={userData.address} // this will cause an error because of taintUniqueValue
/>
)
}
了解有关 安全和服务器操作 的更多信息。
¥Learn more about Security and Server Actions.