<CodeLearn/>
Next.js レッスン3

データ取得

Server ComponentsとClient Componentsの違いを理解し、効率的にデータを取得しよう

Server Components vs Client Components

Next.jsのApp Routerでは、コンポーネントはデフォルトでServer Componentです。 Server Componentはサーバー側で実行され、HTMLとしてクライアントに送信されます。 一方、ユーザーインタラクション(クリック、入力など)が必要な場合は"use client"ディレクティブを 使ってClient Componentにします。

// Server Component(デフォルト)
// - サーバー側で実行される
// - データベースやAPIに直接アクセスできる
// - useState、useEffect は使えない
// - onClick などのイベントハンドラは使えない
export default async function ArticlePage() {
  // サーバー側で直接データを取得
  const article = await fetch("https://api.example.com/articles/1");
  const data = await article.json();

  return (
    <article>
      <h1>{data.title}</h1>
      <p>{data.content}</p>
    </article>
  );
}

// Client Component("use client" を先頭に記述)
// - ブラウザ側で実行される
// - useState、useEffect が使える
// - onClick などのイベントハンドラが使える
// - サーバー側の機能(fs、DBなど)にはアクセスできない
"use client";
import { useState } from "react";

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button onClick={() => setLikes(likes + 1)}>
      ♥ {likes}
    </button>
  );
}

Server Component を使う場面

  • データの取得・表示
  • バックエンドリソースへのアクセス
  • 機密情報(APIキーなど)を扱う処理
  • 大きな依存関係をサーバーに留める

Client Component を使う場面

  • イベントハンドラ(onClick、onChange)
  • useState、useEffect などのHooks
  • ブラウザAPIへのアクセス
  • インタラクティブなUI要素

Server Componentでのデータ取得

Server Componentでは、コンポーネント関数をasyncにして 直接awaitでデータを取得できます。 useEffectやuseStateを使う必要がなく、非常にシンプルです。

// app/users/page.tsx
// Server Componentでのデータ取得(シンプル!)

type User = {
  id: number;
  name: string;
  email: string;
};

export default async function UsersPage() {
  // サーバー側でfetchが実行される
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  const users: User[] = await response.json();

  return (
    <div>
      <h1>ユーザー一覧</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <strong>{user.name}</strong>
            <span>{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

// 比較: React(Client Component)での従来の方法
// "use client";
// import { useState, useEffect } from "react";
//
// export default function UsersPage() {
//   const [users, setUsers] = useState([]);
//   const [loading, setLoading] = useState(true);
//
//   useEffect(() => {
//     fetch("https://jsonplaceholder.typicode.com/users")
//       .then(res => res.json())
//       .then(data => {
//         setUsers(data);
//         setLoading(false);
//       });
//   }, []);
//
//   if (loading) return <p>読み込み中...</p>;
//   return <ul>{users.map(...)}</ul>;
// }

Server Componentを使うと、ローディング状態の管理やuseEffectの依存配列を気にする必要がなくなります。 コードが大幅にシンプルになることがわかります。

キャッシュと再検証(Revalidation)

Next.jsのfetchは拡張されており、キャッシュの動作を細かく制御できます。 データの鮮度に応じて適切なキャッシュ戦略を選びましょう。

// デフォルト: キャッシュあり(静的レンダリング)
// ビルド時に一度だけfetchし、結果をキャッシュ
const data = await fetch("https://api.example.com/posts");

// キャッシュなし(動的レンダリング)
// リクエストごとに毎回fetchする
const data = await fetch("https://api.example.com/posts", {
  cache: "no-store",
});

// 時間ベースの再検証(ISR: Incremental Static Regeneration)
// 60秒間はキャッシュを使い、60秒後に再取得
const data = await fetch("https://api.example.com/posts", {
  next: { revalidate: 60 },
});

// タグベースの再検証
// 特定のタグを持つキャッシュを手動で無効化できる
const data = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts"] },
});

// Server Actionやroute.tsから手動でキャッシュを無効化
import { revalidateTag, revalidatePath } from "next/cache";

revalidateTag("posts");       // "posts"タグのキャッシュを無効化
revalidatePath("/blog");      // /blog ページのキャッシュを無効化

Suspenseとローディング状態

データ取得に時間がかかる場合、ReactのSuspenseを 使ってローディングUIを表示できます。Next.jsのloading.tsxは 内部的にSuspenseを使っています。

// 方法1: loading.tsx を使う(フォルダ単位)
// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
      <div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
      <div className="h-4 bg-gray-200 rounded w-5/6"></div>
    </div>
  );
}

// 方法2: Suspense を使う(コンポーネント単位、より細かい制御)
import { Suspense } from "react";

// データ取得する非同期コンポーネント
async function RecentPosts() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
  return (
    <ul>
      {posts.map((post: { id: number; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

async function UserProfile() {
  const res = await fetch("https://api.example.com/user/1");
  const user = await res.json();
  return <div>{user.name}</div>;
}

// 親コンポーネントで、それぞれ独立してローディング
export default function DashboardPage() {
  return (
    <div>
      <h1>ダッシュボード</h1>

      {/* ユーザー情報とポスト一覧が独立して読み込まれる */}
      <Suspense fallback={<p>プロフィール読み込み中...</p>}>
        <UserProfile />
      </Suspense>

      <Suspense fallback={<p>記事を読み込み中...</p>}>
        <RecentPosts />
      </Suspense>
    </div>
  );
}

Suspenseを使うと、ページの各セクションが独立して読み込まれます。 ユーザープロフィールの取得が遅くても、記事一覧は先に表示されるため、 ユーザー体験が向上します。これを「ストリーミング」と呼びます。

Server ComponentとClient Componentの組み合わせ

実際のアプリでは、Server ComponentとClient Componentを組み合わせて使います。 データの取得はServer Componentで行い、インタラクティブな部分だけをClient Componentにするのが基本パターンです。

// app/blog/[slug]/page.tsx(Server Component)
import LikeButton from "./like-button";
import CommentSection from "./comment-section";

type Article = {
  title: string;
  content: string;
  likes: number;
};

export default async function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  // サーバーでデータを取得
  const res = await fetch(
    `https://api.example.com/posts/${params.slug}`
  );
  const article: Article = await res.json();

  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.content}</p>

      {/* インタラクティブな部分だけClient Component */}
      <LikeButton initialLikes={article.likes} slug={params.slug} />
      <CommentSection slug={params.slug} />
    </article>
  );
}

// app/blog/[slug]/like-button.tsx(Client Component)
"use client";
import { useState } from "react";

export default function LikeButton({
  initialLikes,
  slug,
}: {
  initialLikes: number;
  slug: string;
}) {
  const [likes, setLikes] = useState(initialLikes);

  const handleLike = async () => {
    setLikes(likes + 1);
    await fetch(`/api/posts/${slug}/like`, { method: "POST" });
  };

  return (
    <button onClick={handleLike}>
      ♥ {likes} いいね
    </button>
  );
}

まとめ

  • Next.jsのコンポーネントはデフォルトでServer Component(サーバー側で実行される)
  • "use client"を先頭に書くとClient Componentになる
  • Server Componentではasync/awaitで直接データを取得できる
  • fetchのキャッシュオプションで、静的・動的・ISRを制御できる
  • Suspenseやloading.tsxで、データ取得中のローディングUIを表示できる
  • データ取得はServer Component、インタラクションはClient Componentという使い分けが基本