Skip to main content

模式和最佳实践

在React和Next.js中获取数据有一些推荐的模式和最佳实践。本页面将介绍一些最常见的模式以及如何使用它们。

在服务器上获取数据

在可能的情况下,我们建议使用Server Components在服务器上获取数据。这使您能够:

  • 直接访问后端数据资源(例如数据库)。
  • 通过阻止敏感信息(例如访问令牌和API密钥)暴露给客户端,从而使应用程序更加安全。
  • 在相同的环境中获取数据并进行渲染。这减少了客户端和服务器之间的往返通信,以及客户端上主线程的工作
  • 使用单个往返而不是在客户端上发起多个单独的请求来执行多个数据获取。
  • 减少客户端服务器瀑布效应
  • 根据您的区域,数据获取还可以更接近您的数据源,减少延迟并提高性能。

然后,您可以使用Server Actions 对数据进行突变或更新。

在需要时获取数据

如果需要在树中的多个组件中使用相同的数据(例如当前用户),则无需全局获取数据,也无需在组件之间传递props。 相反,您可以在需要数据的组件中使用fetch或Reactcache,而无需担心为相同数据发出多个请求的性能影响。

这是可能的,因为fetch请求会自动进行记忆。 了解有关请求记忆的更多信息

note

这也适用于布局,因为不可能在父布局和其子布局之间传递数据。

流式处理

流式处理和Suspense是React的特性, 它们允许您逐渐呈现和逐步将UI的渲染单元流式传输到客户端。

通过Server Components和嵌套布局, 您可以立即呈现不需要特定数据的页面部分, 并为获取数据的页面部分显示加载状态。 这意味着用户无需等待整个页面加载完毕,就可以开始与之交互。

server rendering with streaming

要了解有关流式处理和Suspense的更多信息,请参阅 Loading UIStreaming and Suspense页面。

并行和顺序数据获取

在React组件内获取数据时,需要了解两种数据获取模式顺序并行

sequential parallel data fetching

  • 使用顺序数据获取时,路由中的请求彼此依赖,因此创建瀑布效应。可能有情况下您希望使用这种模式,因为一个获取取决于另一个的结果,或者您希望在下一个获取之前满足某个条件以节省资源。然而,此行为也可能是无意的,导致较长的加载时间。
  • 使用并行数据获取时,路由中的请求会被急切地初始化,并且将同时加载数据。这减少了客户端服务器瀑布效应以及加载数据所需的总时间。

顺序数据获取

如果有嵌套组件,并且每个组件都获取自己的数据,那么如果这些数据请求不同, 那么数据获取将按顺序进行(对于请求相同的数据不适用, 因为它们会自动进行记忆)。

例如,Playlists组件只有在Artist组件完成数据获取后才会开始获取数据, 因为Playlists依赖于artistID prop:

app/artist/[username]/page.tsx
// ...
async function Playlists({ artistID }: { artistID: string }) {
// 等待播放列表
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 }
}) {
// 等待艺术家
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将结果流式传输。

这将阻止整个路由因数据获取而被阻塞,用户将能够与未被阻塞的页面部分进行交互。

阻止数据请求:

防止瀑布的另一种方法是在应用程序的根部全局获取数据,但这将阻止所有位于其下的路由片段的呈现,直到数据加载完成。 这可以描述为“一切或无”。要么你有整个页面或应用程序的所有数据,要么没有。

任何带有await的获取请求都将阻止渲染和数据获取其下的整个树,除非它们被包装在<Suspense>边界中或使用了loading.js。 另一种选择是使用并行数据获取或预加载模式。

并行数据获取

要并行获取数据,可以通过在使用数据的组件之外定义请求,然后从组件内部调用它们来急切地启动请求。 这样做可以通过同时初始化两个请求来节省时间,但是在两个promise都解决之前,用户将无法看到渲染的结果。

在下面的示例中,getArtistgetArtistAlbums函数在Page组件之外定义, 然后在组件内调用,我们等待两个promise解决:

app/artist/[username]/page.tsx
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 }
}) {
// 并行启动两个请求
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)

// 等待promise解决
const [artist, albums] = await Promise.all([artistData, albumsData])

return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}

为了改善用户体验,可以添加Suspense边界 来分隔渲染工作,并尽快显示部分结果。

预加载数据

防止瀑布的另一种方法是使用预加载模式。您可以选择创建一个preload函数以进一步优化并行数据获取。 通过这种方式,您不必将promises作为props传递。preload函数的名称可以是任何名称,因为它是一种模式,而不是API。

components/Item.tsx
import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
// void评估给定的表达式并返回undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
const result = await getItem(id)
// ...
}
app/item/[id]/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// 开始加载项目数据
preload(id)
// 执行另一个异步任务
const isAvailable = await checkIsAvailable()

return isAvailable ? <Item id={id} /> : null
}

使用Reactcacheserver-only和预加载模式

可以结合使用缓存函数,预加载模式和仅服务器包来创建可在整个应用程序中使用的数据获取实用程序。

utils/get-item.ts
import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
void getItem(id)
}

export const getItem = cache(async (id: string) => {
// ...
})

通过这种方法,可以急切地获取数据,缓存响应,并确保此数据获取 仅在服务器上发生

utils/get-item导出可以由布局、页面或其他组件使用,以控制何时获取项目数据。

值得知道: 我们建议使用server-only包确保服务器数据获取函数永远不会在客户端上使用。

防止敏感数据暴露给客户端

我们建议使用React的污染APIs, taintObjectReferencetaintUniqueValue, 以防止整个对象实例或敏感值被传递到客户端。

要在应用程序中启用污染,请将Next.js Config的experimental.taint选项设置为true

next.config.js
module.exports = {
experimental: {
taint: true,
},
}

然后将要污染的对象或值传递给experimental_taintObjectReferenceexperimental_taintUniqueValue函数:

app/utils.ts
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 phone number to the client",
data,
data.phoneNumber
)
return data
}
app/page.tsx
import { getUserData } from './data'

export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // 这将由于taintObjectReference而导致错误
phoneNumber={userData.phoneNumber} // 这将由于taintUniqueValue而导致错误
/>
)
}

了解更多关于安全性和服务器操作的信息。