データ取得
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という使い分けが基本