デザインについての学習メモブログ

Next.js 入門 #4: データフェッチ入門 — Server Componentで外部APIを叩く

記事内に広告が含まれています。

Next.js 入門 #4: データフェッチ入門 — Server Componentで外部APIを叩く

前回の記事でServer ComponentとClient Componentの違いを学びました。

今回はServer Componentの真価を発揮する場面、外部APIからデータを取得して表示する方法を学びます。

従来のReact(SPA)では、データ取得といえば useEffect の中で fetch を呼ぶのが定番でした。

Next.jsのServer Componentでは、その必要がありません。

コンポーネント自体を async 関数にして、直接 await fetch() を書くだけです。

基本:async Server Componentでデータを取得する

まず一番シンプルな例を見てみましょう。

今回は無料で使える JSONPlaceholder というダミーAPIを使います。

TSX
// app/posts/page.tsx

type Post = {
  id: number;
  title: string;
  body: string;
};

export default async function PostsPage() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts: Post[] = await res.json();

  return (
    <main>
      <h1>投稿一覧</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <strong>{post.title}</strong>
          </li>
        ))}
      </ul>
    </main>
  );
}

useEffectuseState も不要です。

コンポーネントを async にして await fetch() を呼ぶだけ——これがServer Componentのデータ取得の基本形です。

サーバー上でデータを取得してからHTMLを生成するので、ブラウザには最初から完成した状態のページが届きます。

ローディング中の表示 — loading.tsx

データの取得には時間がかかります。

その間ユーザーに何も見せないのは体験が悪いですよね。

Next.jsでは loading.tsx というファイルを置くだけで、データ取得中のUIを表示できます。

Bash
app/
└── posts/
    ├── page.tsx        データ取得・表示
    └── loading.tsx     読み込み中に表示される
TSX
// app/posts/loading.tsx

export default function Loading() {
  return (
    <main>
      <h1>投稿一覧</h1>
      <p>読み込み中...</p>
    </main>
  );
}

page.tsx のデータ取得が完了するまでの間、自動的に loading.tsx の内容が表示されます。

スケルトンスクリーン(グレーのプレースホルダー)を使うとよりリッチな表現ができます。

TSX
// app/posts/loading.tsx(スケルトン版)

export default function Loading() {
  return (
    <main>
      <h1>投稿一覧</h1>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {[...Array(5)].map((_, i) => (
          <li
            key={i}
            style={{
              height: '1.5rem',
              background: '#e0e0e0',
              borderRadius: '4px',
              marginBottom: '0.75rem',
              animation: 'pulse 1.5s ease-in-out infinite',
            }}
          />
        ))}
      </ul>
    </main>
  );
}

エラーのハンドリング — error.tsx

ネットワークエラーやAPIのレスポンスが不正だった場合の表示も、専用ファイルで管理できます。

Bash
app/
└── posts/
    ├── page.tsx
    ├── loading.tsx
    └── error.tsx       エラー時に表示される
TSX
// app/posts/error.tsx
"use client"; // error.tsx は必ず Client Component にする

import { useEffect } from 'react';

type Props = {
  error: Error;
  reset: () => void; // もう一度試すための関数
};

export default function Error({ error, reset }: Props) {
  useEffect(() => {
    console.error(error);
  }, [error]);

  return (
    <main>
      <h2>データの読み込みに失敗しました</h2>
      <p>{error.message}</p>
      <button onClick={reset}>もう一度試す</button>
    </main>
  );
}

注意: error.tsx は必ず "use client" にする必要があります。

エラー情報の受け取りやリトライ処理にブラウザ側の動きが必要なためです。

試しに、app/posts/page.tsxのfetchで指定したurlに適当な一文字を加えるなどしてわざとエラーを出すと動作の確認ができます。

キャッシュ戦略 — データをいつ再取得するか

Next.jsの fetch は、ブラウザの fetch を拡張していて、キャッシュの制御ができます。

これがNext.jsのデータ取得を理解するうえで外せないポイントです。

デフォルト(キャッシュあり)

TSX
const res = await fetch('https://api.example.com/posts');
// デフォルトではビルド時にキャッシュされ、再取得しない

毎回最新データを取得する

TSX
const res = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
});

リアルタイム性が求められるデータ(在庫情報、最新ニュースなど)に使います。

一定時間ごとに再取得する

TSX
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }, // 60秒ごとに再取得
});

頻繁に変わらないが定期的に更新したいデータ(ブログ記事一覧など)に向いています。

⚠️ cacheオプションとnextオプション

Next.jsはWeb fetch()APIを拡張します。

nextオプションはnextjsの仕様でJavaSctiptの標準仕様では無いです。

Next.js以外では使えない可能性があります。

cacheオプションはJavascriptの標準仕様です。

使い分けの目安

データの性質設定
ほぼ変わらない(会社概要など)デフォルト(キャッシュ)
定期的に更新される(ブログ記事など)revalidate: N(秒)
常に最新が必要(在庫・株価など)cache: 'no-store'

詳細ページでデータを取得する

動的ルート([id])のページでも同じようにデータを取得できます。

記事2で作ったブログ詳細ページに実際のデータ取得を組み合わせてみましょう。

TSX
// app/posts/[id]/page.tsx

import { notFound } from 'next/navigation';

type Post = {
  id: number;
  title: string;
  body: string;
};

type Props = {
  params: Promise<{ id: string }>;
};

export default async function PostPage({ params }: Props) {
  const { id } = await params;

  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

  if (!res.ok) {
    notFound();
  }

  const post: Post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

res.okfalse(404や500など)のときに notFound() を呼ぶことで、記事#2で作った not-found.tsx のページが表示されます。

一覧ページから詳細ページへ繋げる

ここまでの内容を組み合わせて、一覧 → 詳細の流れを完成させましょう。

TSX
// app/posts/page.tsx

import Link from 'next/link';

type Post = {
  id: number;
  title: string;
  body: string;
};

export default async function PostsPage() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
    next: { revalidate: 60 },
  });
  const posts: Post[] = await res.json();

  // 表示数を絞る
  const recentPosts = posts.slice(0, 10);

  return (
    <main>
      <h1>投稿一覧</h1>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {recentPosts.map((post) => (
          <li key={post.id} style={{ marginBottom: '1rem', borderBottom: '1px solid #eee', paddingBottom: '1rem' }}>
            <Link href={`/posts/${post.id}`}>
              <strong>{post.title}</strong>
            </Link>
            <p style={{ color: '#666', marginTop: '0.25rem' }}>
              {post.body.slice(0, 80)}...
            </p>
          </li>
        ))}
      </ul>
    </main>
  );
}

Suspenseで部分的なローディングを制御する

loading.tsx はページ全体のローディング表示でした。

ページの一部だけをローディング状態にしたい場合は、Reactの <Suspense> を使います。

たとえばトップページに「最新の投稿」セクションだけを非同期で読み込みたい場合:

TSX
// components/LatestPosts.tsx(Server Component)

type Post = { id: number; title: string };

export default async function LatestPosts() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5',  {
    cache: "no-store",
  });
  const posts: Post[] = await res.json();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
TSX
// app/page.tsx

import { Suspense } from 'react';
import LatestPosts from '@/components/LatestPosts';

export default function Home() {
  return (
    <main>
      <h1>ようこそ</h1>
      <p>このサイトはNext.jsで作られています。</p>

      <h2>最新の投稿</h2>
      <Suspense fallback={<p>読み込み中...</p>}>
        <LatestPosts />
      </Suspense>
    </main>
  );
}

<Suspense> でラップしたコンポーネントのデータ取得が完了するまでの間、fallback に指定した内容が表示されます。

ページの他の部分はすでに表示されているので、ユーザーは待たされている感覚を受けにくくなります。

ここまでのフォルダ構成

Bash
app/
├── page.tsx                   トップページ(Suspense を使用)
└── posts/
    ├── page.tsx               投稿一覧(fetch + revalidate)
    ├── loading.tsx            読み込み中の表示
    ├── error.tsx              エラー時の表示
    └── [id]/
        └── page.tsx           投稿詳細(動的データ取得)

components/
└── LatestPosts.tsx            部分的なデータ取得コンポーネント

まとめ

この記事で学んだこと:

  • Server Componentを async にして await fetch() を書くだけでデータ取得できる
  • loading.tsx を置くだけでローディング中のUIを表示できる
  • error.tsx"use client" 必須)でエラー時のUIを管理できる
  • fetchcache オプションでデータの再取得タイミングを制御できる
  • <Suspense> でページの一部だけをローディング状態にできる

次の記事では、Next.jsのRoute Handlerを使ってAPIエンドポイント自体を作る方法を学びます。

「フロントエンドのコードだけでAPIも書ける」という感覚を体験しましょう。