数据获取
在 App Router 里,获取数据是最自然的事情:服务端组件直接 await,客户端组件用 use、SWR 或 React Query。本章覆盖两端的获取方式,以及用 Suspense 流式推送不确定耗时的部分。
在服务端组件获取数据
使用 fetch
把组件写成 async 函数,直接 await:
export default async function Page() {
const res = await fetch('https://api.vercel.app/blog');
const posts = await res.json();
return (
<ul>
{posts.map((p: any) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
要点:
- React 会自动对同一组件树里相同的
fetch做 memoization,你可以在真正需要数据的组件里取,不用层层传 props。 fetch默认 不缓存。如需缓存,请搭配use cache指令(第 10 章)或在fetch上配合next: { revalidate }。
使用 ORM 或数据库客户端
既然服务端组件只在服务端跑,你可以直接调用 ORM:
import { db, posts } from '@/lib/db';
export default async function Page() {
const all = await db.select().from(posts);
return (
<ul>
{all.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
仍需对每个查询做鉴权与授权,详见官方 Data Security 指南。
流式渲染
若某些数据较慢,让它们先流着,页面其余部分立刻可见。
loading.js
在路由文件夹里放 loading.tsx,Next.js 会把整页包在 Suspense 边界里:
// app/blog/loading.tsx
export default function Loading() {
return <div>加载中…</div>;
}
用 <Suspense> 精细控制
只让慢的片段走流式:
import { Suspense } from 'react';
import BlogList from '@/components/BlogList';
import BlogListSkeleton from '@/components/BlogListSkeleton';
export default function BlogPage() {
return (
<div>
<header>
<h1>欢迎来到博客</h1>
</header>
<main>
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
);
}
什么是「有意义的加载态」
不要一句 "Loading…" 糊弄用户:骨架屏、占位卡片、封面图和标题都是更好的选择,能让用户感觉到应用在稳定响应。
在客户端组件获取数据
React 的 use API:服务端→客户端流式
先在服务端发起 Promise,把 Promise 传给客户端组件,客户端 use() 读取:
// 服务端组件
import Posts from '@/components/Posts';
import { Suspense } from 'react';
export default function Page() {
const postsPromise = fetch('https://api.vercel.app/blog').then(r => r.json());
return (
<Suspense fallback={<p>Loading…</p>}>
<Posts postsPromise={postsPromise} />
</Suspense>
);
}
'use client';
import { use } from 'react';
export default function Posts({ postsPromise }: { postsPromise: Promise<any[]> }) {
const posts = use(postsPromise);
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
社区库:SWR / React Query
客户端的缓存、重试、分页等场景,用 SWR 或 TanStack Query 最顺手:
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function Posts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher);
if (isLoading) return <p>加载中…</p>;
if (error) return <p>加载失败</p>;
return (
<ul>
{data.map((p: any) => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
与 Nuxt 对比
Nuxt 里你会用 useFetch / useAsyncData;Next.js 的「原生 fetch」其实更贴近标准。两者的共同点是:在服务端拿到的数据序列化到 HTML 中,客户端复用,首屏 0 往返。