数据获取

在 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 会自动对同一组件树里相同的 fetchmemoization,你可以在真正需要数据的组件里取,不用层层传 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 往返。

資料獲取

在 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 會自動對同一元件樹中相同的 fetchmemoization,你可以在真正需要的元件中取,不必逐層傳 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」反而更貼近 Web 標準。兩者共同點是:伺服器端取得的資料序列化至 HTML 中,客戶端重用,首屏 0 往返。

Fetching Data

Fetching data in the App Router is remarkably natural: Server Components await directly; Client Components use use, SWR, or React Query. This chapter covers both sides, plus using Suspense to stream the slow parts.

Fetching in Server Components

With fetch

Make the component async and await the request:

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>
  );
}

Key points:

  • React automatically memoizes identical fetch calls within one component tree, so you can fetch where you need instead of drilling props.
  • fetch is not cached by default. Use the use cache directive (chapter 10) or next: { revalidate } on the fetch to cache.

With an ORM or database client

Server Components only run on the server, so it's safe to query your database directly:

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>
  );
}

Still authenticate and authorize each query. See the Data Security guide.

Streaming

When some data is slow, stream it while the rest of the page renders instantly.

loading.js

Drop a loading.tsx in a route folder to wrap the whole page in a Suspense boundary:

// app/blog/loading.tsx
export default function Loading() {
  return <div>Loading…</div>;
}

<Suspense> for fine-grained control

Stream only the slow fragment:

import { Suspense } from 'react';
import BlogList from '@/components/BlogList';
import BlogListSkeleton from '@/components/BlogListSkeleton';

export default function BlogPage() {
  return (
    <div>
      <header>
        <h1>Welcome to the Blog</h1>
      </header>
      <main>
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  );
}

Design meaningful loading states

Don't ship a bare "Loading…" string. Skeletons, placeholder cards, cover images, and titles all reassure the user that the app is responding.

Fetching in Client Components

Server → Client streaming with use

Kick off the promise on the server, pass it as a prop, and consume it with React's use:

// Server Component
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>
  );
}

Community libraries: SWR / React Query

For client-side caching, retries, pagination, and mutations, SWR or TanStack Query are the ergonomic choices:

'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>Loading…</p>;
  if (error) return <p>Failed to load</p>;
  return (
    <ul>
      {data.map((p: any) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

Nuxt comparison

Nuxt reaches for useFetch / useAsyncData; Next.js leans on native fetch and async components, which is closer to web standards. Both share a goal: data fetched on the server is serialized into the HTML and rehydrated on the client — no round-trip on first paint.