第5部:高度なフロントエンド Step 16 / 24

データフェッチ

Next.jsでのデータ取得方法を学びます。サーバーコンポーネントとクライアントコンポーネントの使い分けがポイントです。

サーバーコンポーネントでのフェッチ

app/posts/page.tsx(推奨)

// サーバーコンポーネント(デフォルト)
// "use client" を書かなければサーバーコンポーネント

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

async function getPosts(): Promise<Post[]> {
  const res = await fetch(`${process.env.API_URL}/posts`, {
    // キャッシュ戦略
    cache: "no-store",  // 常に最新を取得
    // または
    // next: { revalidate: 60 }  // 60秒ごとに再検証
  });

  if (!res.ok) {
    throw new Error("Failed to fetch posts");
  }

  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div className="container mx-auto px-6 py-8">
      <h1 className="text-2xl font-bold mb-6">記事一覧</h1>
      <ul className="space-y-4">
        {posts.map((post) => (
          <li key={post.id} className="bg-white p-4 rounded shadow">
            <h2 className="font-bold">{post.title}</h2>
            <p className="text-gray-600">{post.content.slice(0, 100)}...</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

サーバーコンポーネントのメリット

  • ・データ取得がサーバー側で完結(セキュア)
  • ・初期表示が速い(クライアントJS不要)
  • ・SEOに有利

クライアントコンポーネント + SWR

インストール

npm install swr

components/PostList.tsx

"use client";

import useSWR from "swr";

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

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export function PostList() {
  const { data, error, isLoading, mutate } = useSWR<Post[]>(
    `${process.env.NEXT_PUBLIC_API_URL}/posts`,
    fetcher
  );

  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラーが発生しました</div>;
  if (!data) return null;

  return (
    <div>
      <button
        onClick={() => mutate()}  // 手動で再取得
        className="mb-4 text-blue-600"
      >
        更新
      </button>

      <ul className="space-y-4">
        {data.map((post) => (
          <li key={post.id} className="bg-white p-4 rounded shadow">
            <h2 className="font-bold">{post.title}</h2>
          </li>
        ))}
      </ul>
    </div>
  );
}

SWRの機能

  • ・自動キャッシュ
  • ・フォーカス時の自動再検証
  • ・エラー時の自動リトライ
  • ・リアルタイム更新

認証付きフェッチ

"use client";

import useSWR from "swr";
import { useAuth } from "@/contexts/AuthContext";

export function MyPosts() {
  const { token } = useAuth();

  const fetcher = (url: string) =>
    fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    }).then((res) => {
      if (!res.ok) throw new Error("Fetch error");
      return res.json();
    });

  // tokenがある時だけフェッチ(nullを渡すとフェッチしない)
  const { data, error, isLoading } = useSWR(
    token ? `${process.env.NEXT_PUBLIC_API_URL}/posts/mine` : null,
    fetcher
  );

  if (!token) return <div>ログインしてください</div>;
  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラーが発生しました</div>;

  return (
    <ul>
      {data?.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

キャッシュ戦略

設定 動作 用途
cache: "force-cache" キャッシュを優先 静的コンテンツ
cache: "no-store" 毎回取得 リアルタイムデータ
next: { revalidate: 60 } 60秒ごとに再検証 ある程度新しさが必要

エラー処理(error.tsx)

app/posts/error.tsx

"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div className="text-center py-8">
      <h2 className="text-xl font-bold text-red-600 mb-4">
        エラーが発生しました
      </h2>
      <p className="text-gray-600 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="bg-blue-600 text-white px-4 py-2 rounded"
      >
        もう一度試す
      </button>
    </div>
  );
}

まとめ

  • サーバーコンポーネントでの取得が基本(SEO、セキュリティ)
  • インタラクティブな更新が必要ならSWR
  • 適切なキャッシュ戦略を選択
  • error.tsxでエラー境界を設定
フォーム処理 次へ:エラーハンドリング