本页面将指导你如何在服务器和客户端组件中获取数据,以及如何流式传输依赖数据的组件。
你可以在服务器组件中使用任何异步 I/O 来获取数据,例如:
fetch API要使用 fetch API 获取数据,请将你的组件转换为异步函数,并 await fetch 调用。例如:
export default async function Page() {
const data = await fetch("https://api.vercel.app/blog");
const posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}export default async function Page() {
const data = await fetch("https://api.vercel.app/blog");
const posts = await data.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}须知:
fetch响应默认不缓存。但是,Next.js 会预渲染路由,并且输出将被缓存以提高性能。如果你想选择动态渲染,请使用{ cache: 'no-store' }选项。请参阅fetchAPI 参考。- 在开发过程中,你可以记录
fetch调用以获得更好的可见性和调试效果。请参阅loggingAPI 参考。
由于服务器组件在服务器上渲染,你可以安全地使用 ORM 或数据库客户端进行数据库查询。将你的组件转换为异步函数,并 await 该调用:
import { db, posts } from "@/lib/db";
export default async function Page() {
const allPosts = await db.select().from(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}import { db, posts } from "@/lib/db";
export default async function Page() {
const allPosts = await db.select().from(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}有两种方法可以在客户端组件中获取数据,使用:
use Hookuse Hook 流式传输数据你可以使用 React 的 use Hook 将数据从服务器流式传输到客户端。首先在服务器组件中获取数据,然后将 Promise 作为 prop 传递给你的客户端组件:
import Posts from "@/app/ui/posts";
import { Suspense } from "react";
export default function Page() {
// Don't await the data fetching function
const posts = getPosts();
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
);
}import Posts from "@/app/ui/posts";
import { Suspense } from "react";
export default function Page() {
// Don't await the data fetching function
const posts = getPosts();
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
);
}然后,在你的客户端组件中,使用 use Hook 读取 Promise:
"use client";
import { use } from "react";
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>;
}) {
const allPosts = use(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}"use client";
import { use } from "react";
export default function Posts({ posts }) {
const allPosts = use(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}在上面的示例中,<Posts> 组件被包裹在 <Suspense> 边界内。这意味着在 Promise 正在解析时将显示回退(fallback)。了解更多关于流式传输的信息。
你可以使用社区库,例如 SWR 或 React Query,在客户端组件中获取数据。这些库有自己用于缓存、流式传输和其他功能的语义。例如,使用 SWR:
"use client";
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((r) => r.json());
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
"https://api.vercel.app/blog",
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}"use client";
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((r) => r.json());
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
"https://api.vercel.app/blog",
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}请求去重 fetch 调用的一种方法是使用请求记忆化。通过这种机制,在单个渲染过程中,使用相同 URL 和选项的 GET 或 HEAD fetch 调用会被合并为一个请求。这是一个自动发生的过程,你可以通过向 fetch 传递一个 Abort 信号来选择退出。
请求记忆化(Request memoization)的作用域是请求的生命周期。
你还可以通过使用 Next.js 的数据缓存来对 fetch 请求进行去重,例如在 fetch 选项中设置 cache: 'force-cache'。
数据缓存允许在当前渲染过程和后续请求之间共享数据。
如果你没有使用 fetch,而是直接使用 ORM 或数据库,你可以使用 React cache 函数封装你的数据访问。
import { cache } from "react";
import { db, posts, eq } from "@/lib/db";
export const getPost = cache(async (id: string) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
});
});import { cache } from "react";
import { db, posts, eq } from "@/lib/db";
import { notFound } from "next/navigation";
export const getPost = cache(async (id) => {
const post = await db.query.posts.findFirst({
where: eq(posts.id, parseInt(id)),
});
});警告: 以下内容假定你的应用中启用了
cacheComponents配置选项。该标志是在 Next.js 15 canary 版本中引入的。
当你在服务器组件中获取数据时,数据会在每次请求时在服务器上获取并渲染。如果你有任何缓慢的数据请求,整个路由将被阻塞,直到所有数据获取完成才能渲染。
为了提高初始加载时间和用户体验,你可以使用流式传输将页面的 HTML 分解成更小的块,并逐步将这些块从服务器发送到客户端。

你可以通过两种方式在应用程序中利用流式传输:
loading.js 文件包装页面<Suspense> 包装组件loading.js你可以在与页面相同的文件夹中创建一个 loading.js 文件,以便在数据获取期间流式传输整个页面。例如,要流式传输 app/blog/page.js,请将该文件添加到 app/blog 文件夹中。

export default function Loading() {
// Define the Loading UI here
return <div>Loading...</div>;
}export default function Loading() {
// Define the Loading UI here
return <div>Loading...</div>;
}导航时,用户会立即看到布局和一个加载状态,而页面正在渲染。渲染完成后,新内容将自动替换进来。

在幕后,loading.js 将嵌套在 layout.js 中,并将自动用 <Suspense> 边界包装 page.js 文件及其下面的所有子项。

这种方法适用于路由段(布局和页面),但如果需要更细粒度的流式传输,可以使用 <Suspense>。
<Suspense><Suspense> 允许你更细粒度地控制页面哪些部分进行流式传输。例如,你可以立即显示 <Suspense> 边界之外的任何页面内容,并流式传输边界内的博客文章列表。
import { Suspense } from "react";
import BlogList from "@/components/BlogList";
import BlogListSkeleton from "@/components/BlogListSkeleton";
export default function BlogPage() {
return (
<div>
{/* This content will be sent to the client immediately */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* If there's any dynamic content inside this boundary, it will be streamed in */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
);
}import { Suspense } from "react";
import BlogList from "@/components/BlogList";
import BlogListSkeleton from "@/components/BlogListSkeleton";
export default function BlogPage() {
return (
<div>
{/* This content will be sent to the client immediately */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* If there's any dynamic content inside this boundary, it will be streamed in */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
);
}即时加载状态是导航后立即向用户显示的回退 UI。为了获得最佳用户体验,我们建议设计有意义的加载状态,帮助用户了解应用程序正在响应。例如,你可以使用骨架屏和加载指示器,或者未来屏幕的一小部分但有意义的内容,例如封面照片、标题等。
在开发过程中,你可以使用 React Devtools 预览和检查组件的加载状态。
顺序数据获取发生在一个请求依赖于另一个请求的数据时。
例如,<Playlists> 只有在 <Artist> 完成后才能获取数据,因为它需要 artistID:
export default async function Page({
params,
}: {
params: Promise<{ username: string }>;
}) {
const { username } = await params;
// Get artist information
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
{/* Show fallback UI while the Playlists component is loading */}
<Suspense fallback={<div>Loading...</div>}>
{/* Pass the artist ID to the Playlists component */}
<Playlists artistID={artist.id} />
</Suspense>
</>
);
}
async function Playlists({ artistID }: { artistID: string }) {
// Use the artist ID to fetch 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 }) {
const { username } = await params;
// Get artist information
const artist = await getArtist(username);
return (
<>
<h1>{artist.name}</h1>
{/* Show fallback UI while the Playlists component is loading */}
<Suspense fallback={<div>Loading...</div>}>
{/* Pass the artist ID to the Playlists component */}
<Playlists artistID={artist.id} />
</Suspense>
</>
);
}
async function Playlists({ artistID }) {
// Use the artist ID to fetch playlists
const playlists = await getArtistPlaylists(artistID);
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
);
}在此示例中,<Suspense> 允许在艺术家数据加载后流式传输播放列表。然而,页面仍然需要等待艺术家数据才能显示任何内容。为了防止这种情况,你可以将整个页面组件包装在 <Suspense> 边界中(例如,使用 loading.js 文件),以立即显示加载状态。
确保你的数据源能快速解析第一个请求,因为它会阻塞所有其他内容。如果你无法进一步优化请求,并且数据不经常更改,请考虑缓存结果。
当路由中的数据请求被主动发起并同时开始时,就会发生并行数据获取。
默认情况下,布局和页面是并行渲染的。因此,每个段会尽快开始获取数据。
然而,在任何组件内部,如果多个 async/await 请求按顺序放置,它们仍然是顺序执行的。例如,getAlbums 将被阻塞,直到 getArtist 解析完成:
import { getArtist, getAlbums } from "@/app/lib/data";
export default async function Page({ params }) {
// These requests will be sequential
const { username } = await params;
const artist = await getArtist(username);
const albums = await getAlbums(username);
return <div>{artist.name}</div>;
}通过调用 fetch 来发起多个请求,然后使用 Promise.all 等待它们完成。fetch 调用后请求立即开始。
import Albums from "./albums";
async function getArtist(username: string) {
const res = await fetch(`https://api.example.com/artist/${username}`);
return res.json();
}
async function getAlbums(username: string) {
const res = await fetch(
`https://api.example.com/artist/${username}/albums`
);
return res.json();
}
export default async function Page({
params,
}: {
params: Promise<{ username: string }>;
}) {
const { username } = await params;
// Initiate requests
const artistData = getArtist(username);
const albumsData = getAlbums(username);
const [artist, albums] = await Promise.all([artistData, albumsData]);
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
);
}import Albums from "./albums";
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`);
return res.json();
}
async function getAlbums(username) {
const res = await fetch(
`https://api.example.com/artist/${username}/albums`
);
return res.json();
}
export default async function Page({ params }) {
const { username } = await params;
// Initiate requests
const artistData = getArtist(username);
const albumsData = getAlbums(username);
const [artist, albums] = await Promise.all([artistData, albumsData]);
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
);
}须知: 如果在使用
Promise.all时有一个请求失败,整个操作都将失败。为了处理这种情况,你可以改用Promise.allSettled方法。
你可以通过创建一个工具函数来预加载数据,这个函数可以在阻塞请求之上被主动调用。<Item> 根据 checkIsAvailable() 函数的结果有条件地渲染。
你可以在 checkIsAvailable() 之前调用 preload(),以主动触发 <Item/> 的数据依赖。当 <Item/> 渲染时,其数据已提前获取。
import { getItem, checkIsAvailable } from "@/lib/data";
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// starting loading item data
preload(id);
// perform another asynchronous task
const isAvailable = await checkIsAvailable();
return isAvailable ? <Item id={id} /> : null;
}
const preload = (id: string) => {
// void evaluates the given expression and returns undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id);
};
export async function Item({ id }: { id: string }) {
const result = await getItem(id);
// ...
}import { getItem, checkIsAvailable } from '@/lib/data'
export default async function Page({ params }) {
const { id } = await params
// starting loading item data
preload(id)
// perform another asynchronous task
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
const preload = (id) => {
// void evaluates the given expression and returns undefined
// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
void getItem(id)
}
export async function Item({ id }) {
const result = await getItem(id)
// ...此外,你可以使用 React 的 cache 函数和 server-only 包来创建一个可复用的工具函数。这种方法允许你缓存数据获取函数,并确保它只在服务器上执行。
import { cache } from "react";
import "server-only";
import { getItem } from "@/lib/data";
export const preload = (id: string) => {
void getItem(id);
};
export const getItem = cache(async (id: string) => {
// ...
});import { cache } from "react";
import "server-only";
import { getItem } from "@/lib/data";
export const preload = (id) => {
void getItem(id);
};
export const getItem = cache(async (id) => {
// ...
});