TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

Next.js App Router の基礎:フロントエンドエンジニアのための入門ガイド(コンポーネントとデータ取得編)

1. はじめに

1.1 第 2 回の位置づけ

このシリーズ第 1 回では、Next.js App Router の基本的な概念と、appディレクトリを中心とした新しいルーティングシステムについて学びました。ファイルベースのルーティングがいかに直感的で効率的であるかをご理解いただけたでしょう。

第 2 回となる今回は、App Router の心臓部とも言えるServer ComponentsClient Componentsに焦点を当てます。これらのコンポーネントの特性を理解し、適切に使い分けることは、App Router を使いこなす上で最も重要なスキルです。さらに、Next.js 15 での変更点を踏まえた最新のデータフェッチング戦略についても、具体的なコードを交えながら解説します。

1.2 この記事で身につくスキル

この記事を読み終える頃には、あなたは以下のスキルを習得しているでしょう。

  • Server Components と Client Components の明確な使い分け
    パフォーマンスとインタラクティブ性を両立させるための判断基準が身につきます。
  • 最新のデータフェッチングパターン
    fetch API を活用したサーバーサイドでのデータ取得、キャッシュ戦略、そしてクライアントサイドでのデータ取得方法をマスターします。
  • エラーハンドリングとローディング状態の管理
    error.tsxloading.tsxを使いこなし、堅牢でユーザーフレンドリーな UI を構築できます。
  • 実践的な開発テクニック
    ナビゲーション、メタデータ設定、スタイリングなど、実務で即座に役立つ実践的なテクニックを学べます。

1.3 前提知識と対象読者

本記事は、以下の知識を持つ読者を対象としています。

  • 本シリーズ第 1 回の内容を理解していること
    App Router の基本的なディレクトリ構造とルーティングの知識が前提となります。
  • React Hooks の基本的な使い方
    useState, useEffectなどの基本的なフックに慣れていること。
  • Next.js Pages Router での開発経験(推奨)
    Pages Router のgetServerSidePropsgetStaticPropsを知っていると、App Router との違いがより明確に理解できます。

準備はよろしいでしょうか。それでは、App Router のコンポーネントモデルの深淵へと一緒にダイブしていきましょう。

第1回の記事は以下からご覧いただけます。
Next.js App Router の基礎:フロントエンドエンジニアのための入門ガイド(概念とディレクトリ構造編)

2. Server Components と Client Components

App Router のアーキテクチャを理解する上で最も重要な概念が、Server Components と Client Components です。この 2 つのコンポーネントモデルを理解することが、パフォーマンスとインタラクティブ性を両立させる鍵となります。

2.1 Server Components の特徴と制約

App Router では、すべてのコンポーネントはデフォルトで Server Componentsとして扱われます。これは Pages Router からの大きな変化です。

特徴

  • サーバーサイドでのみ実行
    コンポーネントのコードはクライアントサイドの JavaScript バンドルに含まれず、ページの初期表示速度向上に大きく貢献します。
  • ダイレクトなデータアクセス
    データベースやファイルシステム、内部 API など、サーバー側のリソースに直接アクセスできます。
  • セキュリティ
    API キーやトークンなどの機密情報をクライアントに公開することなく、安全に利用できます。
  • 大きな依存関係の利用
    サーバー側でのみ実行されるため、クライアントのパフォーマンスに影響を与えることなく、大規模なライブラリを利用できます。

制約

  • インタラクティブ性がない
    useStateuseEffectといった React フックや、onClickなどのイベントリスナーは使用できません。
  • ブラウザ API へのアクセス不可
    windowlocalStorageなど、ブラウザ環境に依存する API は利用できません。
// app/components/ServerTime.tsx  
// Server Component(デフォルト)  
export default async function ServerTime() {  
  // このコンポーネントはサーバー側で実行され、  
  // 結果のHTMLのみがクライアントに送信される  
  const response = await fetch(  
    "http://worldtimeapi.org/api/timezone/Asia/Tokyo",  
    {  
      next: { revalidate: 10 }, // 10秒ごとに再検証  
    }  
  );  
  const data = await response.json();  
  const time = new Date(data.datetime).toLocaleTimeString();  
  
  // クライアントが受け取るのは以下のJSXではなく、  
  // レンダリング済みのHTML(例: <div><p>現在の...</p><p>14:30:45</p></div>)  
  return (  
    <div>  
      <p>現在の東京の時刻(サーバー取得):</p>  
      <p>{time}</p>  
    </div>  
  );  
}  

この例では、サーバーサイドで時刻 API を叩き、結果を表示しています。fetchrevalidateオプションにより、10 秒間隔でデータが更新されます。

2.2 Client Components の特徴

インタラクティブな UI を構築するためには、Client Components を使用します。

特徴

  • "use client"ディレクティブ
    ファイルの先頭に"use client";と記述することで、そのファイル内のすべてのコンポーネントが Client Components として扱われます。
  • ブラウザでの実行
    従来の React コンポーネントと同様に、クライアントサイドでレンダリングされ、実行されます。
  • インタラクティブ機能
    useState, useEffect, useContextなどのフックや、イベントリスナーを利用して、ユーザー操作に応じた動的な UI を実現できます。
  • ブラウザ API へのアクセス
    windowオブジェクトやlocalStorageなど、ブラウザの機能を利用できます。
// app/components/CounterButton.tsx  
// Client Component`  
"use client";  
  
import { useState, useEffect } from "react";  
  
export default function CounterButton() {  
  const [count, setCount] = useState(0);  
  
  useEffect(() => {  
    // マウント時にローカルストレージから初期値を読み込む  
    const savedCount = localStorage.getItem("counter");  
    if (savedCount) {  
      setCount(parseInt(savedCount, 10));  
    }  
  }, []);  
  
  const handleClick = () => {  
    const newCount = count + 1;  
    setCount(newCount);  
    localStorage.setItem("counter", newCount.toString());  
  };  
  
  return <button onClick={handleClick}>You clicked {count} times</button>;  
}  

このカウンターボタンは、状態管理(useState)、ライフサイクルイベント(useEffect)、ユーザーイベント(onClick)、ブラウザ API(localStorage)を利用しています。そのため、Client Component として実装する必要があります。

2.3 使い分けの基準と判断方法

どちらのコンポーネントを使うべきか判断する際の基本的な指針は「可能な限り Server Components を使い、インタラクティブ性が必要な部分のみを Client Components にする」です。

判断フロー

  1. データ取得やサーバー処理が主な場合
    Server Component
  2. ユーザーの操作(クリック、入力など)に応答する必要がある場合
    Client Component
  3. useState, useEffectなどのフックが必要な場合
    Client Component
  4. ブラウザ固有の API(window, localStorageなど)が必要な場合
    Client Component
  5. 上記以外の場合
    Server Component

パフォーマンスへの影響

Client Components は、そのコードがクライアントにダウンロードされ、実行されるため、JavaScript バンドルサイズが増加します。そのため、UI のインタラクティブな部分だけを小さな Client Component として切り出すことが重要です。ページの大部分は Server Components で構成することが、パフォーマンス最適化の鍵となります。

2.4 コンポーネント間の連携パターン

Server Components と Client Components は、シームレスに連携させることができます。

Server Component から Client Component への props 渡し

Server Component は Client Component をインポートして利用できます。サーバーで取得したデータをpropsとして渡すことが可能です。

// app/page.tsx (Server Component)  
import CounterButton from "./components/CounterButton";  
import ServerTime from "./components/ServerTime";  
  
export default function Page() {  
  return (  
    <div>  
      <h1>Server and Client Components Demo</h1>  
      <ServerTime />  
      <hr />  
      <CounterButton />  
    </div>  
  );  
}  

Client Component で Server Component をラップする

  • Client Component 内で Server Component を直接importできない
    "use client"ディレクティブを持つファイル内で、Server Component をインポートして使用することはできません。
  • Server Component をpropsとして渡す
    Client Component が Server Component を子要素として受け取ることは可能です。これは「children prop」や他のpropとして渡すことで実現します。
// app/components/ClientWrapper.tsx  
"use client";  
  
import { useState } from "react";  
  
export default function ClientWrapper({  
  children,  
}: {  
  children: React.ReactNode;  
}) {  
  const [isOpen, setIsOpen] = useState(true);  
  
  return (  
    <div style={{ border: "1px solid gray", padding: "1rem" }}>  
      <button onClick={() => setIsOpen(!isOpen)}>  
        {isOpen ? "Hide" : "Show"} Server Content  
      </button>  
      {isOpen && <div>{children}</div>}  
    </div>  
  );  
}  
  
// app/page.tsx (Server Component)  
import ClientWrapper from "./components/ClientWrapper";  
import ServerTime from "./components/ServerTime";  
  
export default function Page() {  
  return (  
    <div>  
      <h1>Server Content wrapped in Client Component</h1>  
      <ClientWrapper>  
        {/* Server Componentをchildrenとして渡す */}  
        <ServerTime />  
      </ClientWrapper>  
    </div>  
  );  
}  

このパターンでは、インタラクティブな開閉ロジックを持つClientWrapperが、サーバーでレンダリングされたServerTimeコンポーネントを子要素として受け取っています。これにより、サーバーとクライアントの役割を明確に分離しつつ、協調させることができます。

このコンポーネントモデルを理解し、適切に境界線を設計することが、App Router 開発の第一歩です。次の章では、これらのコンポーネントを使って実際にデータを取得し、表示する方法を詳しく見ていきましょう。

3. データの取得と表示

App Router では、コンポーネントの種類(Server/Client)に応じてデータ取得の方法が異なります。ここでは、それぞれのパターンとベストプラクティスを学びます。

3.1 Server Component でのデータフェッチ

Server Components はasync/awaitを直接サポートしており、データ取得が直感的になりました。これは、Pages Router のgetServerSidePropsgetStaticPropsがコンポーネント自体に統合されたようなイメージです。

// app/posts/page.tsx  
  
// ページコンポーネントを非同期関数として定義  
export default async function PostList() {  
  // サーバーサイドで直接APIを叩く  
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");  
  
  // エラーハンドリング  
  if (!res.ok) {  
    throw new Error("Failed to fetch posts");  
  }  
  
  const posts = await res.json();  
  
  return (  
    <main>  
      <h1>投稿一覧</h1>  
      <ul>  
        {posts.map((post: any) => (  
          <li key={post.id}>{post.title}</li>  
        ))}  
      </ul>  
    </main>  
  );  
}  

このコードは、ビルド時またはリクエスト時にサーバーサイドで実行され、取得したデータを含む HTML をクライアントに返します。クライアント側での追加の API リクエストは発生しません。

3.2 キャッシュの理解と活用(Next.js 15 対応)

Next.js 15 でのキャッシュ動作の変更について

Next.js 15 では、fetchリクエストのキャッシュ動作が大きく変更されました。デフォルトでfetchリクエストはキャッシュされませんcache: 'no-store'相当の動作)。これにより、常に最新のデータを取得する動的な挙動が基本となりますが、パフォーマンス最適化のためにはキャッシュ戦略を明示的に設定します。

// app/data-fetch-example/page.tsx  
  
export default async function DataFetchExample() {  
  // デフォルト:キャッシュされない(リクエストごとに再フェッチ)  
  const freshDataRes = await fetch("https://api.example.com/data");  
  
  // キャッシュを有効にする場合:明示的に指定が必要  
  const cachedDataRes = await fetch("https://api.example.com/data", {  
    cache: "force-cache", // ビルド時にフェッチし、永続的にキャッシュ  
  });  
  
  // 時間ベースの再検証(ISR: Incremental Static Regeneration)  
  const revalidatedDataRes = await fetch("https://api.example.com/data", {  
    next: { revalidate: 3600 }, // 3600秒(1時間)ごとに再検証  
  });  
  
  // ... 各レスポンスを処理 ...  
  
  return <div>{/* レンダリング */}</div>;  
}  

キャッシュ戦略の選択指針

  • 静的なコンテンツ(ブログ記事、製品情報など)
    cache: 'force-cache'でビルド時にキャッシュし、高速表示を実現します。
  • 頻繁に更新されるが、即時性は不要なデータ(ニュースフィードなど)
    next: { revalidate: <秒数> }で ISR を利用し、定期的にデータを更新します。
  • 常に最新であるべきデータ(株価、在庫情報など)
    デフォルトのキャッシュなし(cache: 'no-store')の挙動を利用します。

この変更により、開発者はデータの鮮度とパフォーマンスのバランスを、より細かく制御できるようになりました。

3.3 Client Component でのデータフェッチ

Client Component でデータを取得する場合、従来通りの方法が使えますが、ライブラリの活用が効果的です。

  • useEffectを使った伝統的なパターン
    コンポーネントのマウント時にデータを取得します。しかし、ローディング状態やエラー処理、キャッシュ管理などを自前で実装する必要があり、複雑になりがちです。
  • SWR や TanStack Query の利用(推奨)
    これらのデータ取得ライブラリは、キャッシュ、再検証、ローディング/エラー状態の管理などを自動で行ってくれるため、コードがシンプルになり、バグも減らせます。
// app/components/UserInfo.tsx  
"use client";  
  
import useSWR from "swr";  
  
// fetcher関数を定義  
const fetcher = (url: string) => fetch(url).then((res) => res.json());  
  
export function UserInfo({ userId }: { userId: string }) {  
  const { data, error, isLoading } = useSWR(`/api/users/${userId}`, fetcher);  
  
  if (isLoading) return <div>読み込み中...</div>;  
  if (error) return <div>エラーが発生しました</div>;  
  if (!data) return null;  
  
  return (  
    <div>  
      <h2>{data.name}</h2>  
      <p>Email: {data.email}</p>  
    </div>  
  );  
}  

Server Component との使い分け

ページの初期表示に必要なデータは Server Components で取得し、ユーザーのアクションに応じて動的に取得するデータ(例:検索結果、無限スクロールの追加データなど)は Client Components で取得するのが一般的なパターンです。

3.4 並列データフェッチと最適化

複数の独立したデータソースから情報を取得する場合、逐次的にawaitするとウォーターフォール問題が発生し、全体の読み込み時間が長くなってしまいます。

// ❌ 非効率なウォーターフォール  
const posts = await fetchPosts(); // 待機  
const users = await fetchUsers(); // postsの完了後に開始  

Promise.allを使うことで、これらのリクエストを並列実行し、全体の待ち時間を短縮できます。

// app/dashboard/page.tsx  
  
import { fetchPosts, fetchUsers, fetchComments } from "@/lib/api";  
  
export default async function Dashboard() {  
  // 複数のデータ取得リクエストを並列で開始  
  const [posts, users, comments] = await Promise.all([  
    fetchPosts(),  
    fetchUsers(),  
    fetchComments(),  
  ]);  
  
  return (  
    <div>  
      <h1>ダッシュボード</h1>  
      <section>  
        <h2>最新の投稿</h2>  
        {/* postsを使ったレンダリング */}  
      </section>  
      <section>  
        <h2>アクティブユーザー</h2>  
        {/* usersを使ったレンダリング */}  
      </section>  
      <section>  
        <h2>最近のコメント</h2>  
        {/* commentsを使ったレンダリング */}  
      </section>  
    </div>  
  );  
}  

このテクニックは、特に Server Components でページの初期表示に必要なデータをまとめて取得する際に効果的です。データ取得の戦略を最適化することで、ユーザーが感じる表示速度を改善できます。

4. エラーハンドリングとローディング状態

堅牢なアプリケーションを構築するには、データの読み込み中やエラー発生時の状態を適切にユーザーに伝えることが不可欠です。App Router は、loading.tsxerror.tsxという特別なファイルを通じて、これを簡単かつ宣言的に実装する仕組みを提供します。

4.1 エラーハンドリングの基本

特定のルートセグメントでエラーが発生した場合、Next.js は最も近い階層にあるerror.tsxファイルを探してレンダリングします。これにより、エラーがアプリケーション全体をクラッシュさせるのを防ぎ、影響範囲を限定できます。

error.tsxの作成

// app/dashboard/error.tsx  
"use client"; // エラーコンポーネントはClient Componentである必要がある  
  
import { useEffect } from "react";  
  
export default function Error({  
  error,  
  reset,  
}: {  
  error: Error & { digest?: string };  
  reset: () => void;  
}) {  
  useEffect(() => {  
    // エラーをロギングサービスに送信するなどの処理  
    console.error(error);  
  }, [error]);  
  
  return (  
    <div>  
      <h2>おっとぉ、これは思わぬ問題が発生しました。</h2>  
      <p>  
        ダッシュボードの表示中に予期せぬエラーが発生しました。もう一回「再試行」ボタンをポチッとして実行しちゃってー  
      </p>  
      <button  
        onClick={() => reset()} // セグメントの再レンダリングを試みる  
      >  
        再試行  
      </button>  
    </div>  
  );  
}  
  • "use client"が必須である。error.tsxはイベントハンドラ(reset関数)を持つため、Client Component である必要がある
  • error prop には発生したエラーオブジェクトが含まれる
  • reset prop を呼び出すと、エラー境界内のコンポーネント(この場合はpage.tsxなど)の再レンダリングを試みる

notFound()関数

データが見つからないなど、リソースが存在しないことを示す場合は、error.tsxではなくnotFound()関数を使います。これを呼び出すと、最も近いnot-found.tsxファイルがレンダリングされます。

// app/posts/[slug]/page.tsx  
import { notFound } from "next/navigation";  
  
async function getPost(slug: string) {  
  const res = await fetch(`https://api.example.com/posts/${slug}`);  
  if (res.status === 404) {  
    return null;  
  }  
  return res.json();  
}  
  
export default async function PostPage({ params }) {  
  const post = await getPost(params.slug);  
  
  if (!post) {  
    notFound(); // not-found.tsx をレンダリング  
  }  
  
  return <article>{/* ... */}</article>;  
}  

4.2 ローディング状態の管理

loading.tsxは、React Suspense と組み合わせることで、ルートセグメントのコンテンツが読み込まれるまでの間、フォールバック UI(ローディングインジケーターなど)を自動的に表示する仕組みです。

loading.tsxの作成

// app/dashboard/loading.tsx  
  
// スケルトンUIコンポーネント  
function SkeletonCard() {  
  return (  
    <div className="skeleton-card">  
      <div className="skeleton-title"></div>  
      <div className="skeleton-text"></div>  
      <div className="skeleton-text"></div>  
    </div>  
  );  
}  
  
export default function Loading() {  
  // このUIは、page.tsxの読み込み中に表示される  
  return (  
    <div>  
      <h1>読み込み中...</h1>  
      <SkeletonCard />  
      <SkeletonCard />  
      <SkeletonCard />  
    </div>  
  );  
}  
  • 自動的な Suspense 境界
    loading.tsxを作成すると、同じ階層のpage.tsxとその子要素が自動的に<Suspense>でラップされる
  • ストリーミングレンダリング
    サーバーはまずレイアウトなどの静的な部分とローディング UI を送信し、データの読み込みが完了次第、page.tsxのコンテンツをストリーミングで送信する。これにより、ユーザーはコンテンツを待っている間もインタラクションが可能になり、体感速度が向上する

Suspense による部分的ローディング

ページ全体ではなく、特定のコンポーネントのみにローディング状態を設定したい場合は、手動で<Suspense>コンポーネントを使用します。

// app/dashboard/page.tsx  
import { Suspense } from "react";  
import { PostFeed, WeatherWidget } from "./components";  
  
export default function Dashboard() {  
  return (  
    <main>  
      <h1>ダッシュボード</h1>  
      <Suspense fallback={<p>投稿フィードを読み込み中...</p>}>  
        <PostFeed />  
      </Suspense>  
      <Suspense fallback={<p>天気情報を読み込み中...</p>}>  
        <WeatherWidget />  
      </Suspense>  
    </main>  
  );  
}  

この例では、PostFeedWeatherWidgetがそれぞれ独立してデータを取得し、先に準備ができた方から表示されます。これにより、遅いデータ取得がページ全体のレンダリングをブロックするのを防ぎます。

5. 実践的な開発パターン

ここまでの知識を統合し、実際の開発で頻出するパターンを見ていきましょう。

5.1 ナビゲーションの実装

App Router での画面遷移は、主にLinkコンポーネントとuseRouterフックで行います。

<Link>コンポーネント

<Link>コンポーネントがページ間を移動するための基本的な方法です。<a>タグのラッパーとして機能し、プリフェッチ(バックグラウンドでのページ読み込み)などの最適化を自動で行います。

import Link from "next/link";  
  
function Header() {  
  return (  
    <nav>  
      <Link href="/">ホーム</Link>  
      <Link href="/about">会社概要</Link>  
      <Link href="/posts/hello-world">ブログ記事へ</Link>  
    </nav>  
  );  
}  

useRouterフック

useRouterフックは Client Component 内で、イベントハンドラなどからプログラム的に画面遷移する場合に使用されます。

"use client";  
import { useRouter } from "next/navigation";  
  
function SearchButton() {  
  const router = useRouter();  
  
  const handleSearch = () => {  
    // 検索処理...  
    router.push("/search-results");  
  };  
  
  return <button onClick={handleSearch}>検索</button>;  
}  

5.2 メタデータの設定

ページの<head>タグ内に配置されるメタデータ(タイトル、説明文、OGP タグなど)は、SEO や SNS での共有において非常に重要です。App Router では、metadataオブジェクトまたはgenerateMetadata関数を使ってこれを設定します。

静的メタデータ

静的メタデータはmetadataオブジェクトをエクスポートして設定します。

// app/about/page.tsx  
import type { Metadata } from "next";  
  
export const metadata: Metadata = {  
  title: "会社概要 | Synergy Marketing",  
  description:  
    "シナジーマーケティング株式会社の会社概要ページです。弊社主催のイベントも実施してるので遊びに来てね。",  
};  
  
export default function AboutPage() {  
  /* ... */  
}  

動的メタデータ

動的メタデータはgenerateMetadata関数をエクスポートします。動的ルート(例: [slug])で、パラメータに応じたメタデータを生成する際に使用します。

// app/posts/[slug]/page.tsx  
import type { Metadata } from "next";  
  
async function getPost(slug: string) {  
  /* ... */  
}  
  
export async function generateMetadata({ params }): Promise<Metadata> {  
  const post = await getPost(params.slug);  
  if (!post) {  
    return { title: "記事が見つかりません" };  
  }  
  return {  
    title: post.title,  
    description: post.excerpt,  
    openGraph: {  
      title: post.title,  
      images: [{ url: post.ogImageUrl }],  
    },  
  };  
}  
  
export default async function PostPage({ params }) {  
  /* ... */  
}  

5.3 スタイリングの基本

App Router でも、従来のスタイリング方法が利用できます。

  • グローバルスタイル
    ルートレイアウト(app/layout.tsx)で CSS ファイルをインポートすると、アプリケーション全体に適用される。
  • CSS Modules
    [name].module.cssという形式のファイルを作成し、コンポーネントにインポートして使う。クラス名が自動的に一意になるため、スタイルが競合する心配はない。Server Components と Client Components の両方で利用でき、推奨される方法である。
  • Tailwind CSS
    設定すれば、シームレスに動作する。ユーティリティファーストのアプローチで、迅速な UI 構築が可能である。

5.4 データ取得の効率化テクニック

reactパッケージからインポートできるcache関数は、データ取得処理をメモ化(結果をキャッシュ)するための強力なツールです。同じ引数で複数回呼び出された場合でも、実際のリクエストは一度しか実行されません。

// lib/data.ts  
import { cache } from "react";  
import db from "./db";  
  
// cacheでラップすることで、同じIDでの呼び出しはキャッシュを返す  
export const getUser = cache(async (id: number) => {  
  const user = await db.user.findUnique({ where: { id } });  
  return user;  
});  
  
// app/layout.tsx  
import { getUser } from "@/lib/data";  
  
export default async function Layout({ children }) {  
  const user = await getUser(1);  
  console.log(user.name); // データベースから取得  
  // ...  
}  
  
// app/components/Avatar.tsx  
import { getUser } from "@/lib/data";  
  
export default async function Avatar() {  
  const user = await getUser(1); // データベースにはアクセスせず、キャッシュから取得  
  return <img src={user.avatarUrl} alt={user.name} />;  
}  

この例では、layout.tsxAvatar.tsxの両方でgetUser(1)が呼び出されていますが、cacheのおかげでデータベースへのクエリは 1 回で済みます。これにより、不要なデータ取得を削減し、パフォーマンスを向上させることができます。

6. 開発時のテクニック集

App Router を最大限に活用し、高品質なアプリケーションを効率的に開発するためのテクニック集を紹介します。

6.1 パフォーマンス最適化

  • Server Components をデフォルトに
    常に Server Components から実装を始め、インタラクティブ性が必要になった場合にのみ、その部分を Client Component として切り出すことを徹底します。
  • Client Components の最小化
    Client Component は、できるだけ小さく、UI の末端(葉っぱ)に配置するよう心がけます。これにより、クライアントに送信される JavaScript の量を最小限に抑えられます。
  • 適切なキャッシュ戦略
    Next.js 15 のキャッシュ動作を理解し、データの特性に応じてforce-cacherevalidateno-storeを明示的に使い分けます。
  • next/imageの活用
    画像はnext/imageコンポーネントを使って配信します。自動的な最適化(リサイズ、フォーマット変換、遅延読み込み)により、ページの表示速度が大幅に向上します。

6.2 開発時のデバッグとトラブルシューティング

Server/Client の境界エラー
useStateは Client Component でしか使えません」といったエラーは頻出します。エラーメッセージをよく読み、"use client"ディレクティブが適切か確認します。

  • ハイドレーションエラー
    サーバーでレンダリングされた HTML と、クライアントで初期レンダリングされた UI が一致しない場合に発生します。useEffect内でwindowオブジェクトにアクセスするなど、サーバーとクライアントで結果が異なるコードが原因であることが多いです。
  • キャッシュのデバッグ
    開発サーバーを再起動するとメモリキャッシュはクリアされます。fetchのキャッシュが意図通りに動作しない場合は、cache: 'no-store'を一時的に設定して、キャッシュなしの動作を確認してみるのも有効です。

6.3 TypeScript での型安全性

  • API レスポンスの型定義
    fetchで取得したデータの型をzodなどのバリデーションライブラリを使って検証し、型安全性を確保することを推奨します。
  • generateMetadataの型
    Promise<Metadata>を戻り値の型として指定することで、必須プロパティの漏れを防ぎます。
  • Props の型
    Server Component から Client Component へ渡す Props の型定義をしっかり行い、コンポーネント間のインターフェースを明確にします。

6.4 テスト戦略の基本

  • Server Components のテスト(およびインテグレーションテスト)
    Server Components は Next.js のサーバー環境と密結合しているため、単体でテストすることは難しいです。そのため、後述する E2E テストや、Client Components と結合したインテグレーションテストで動作を確認するのが一般的です。テストをする場合は、Jest や Vitest と React Testing Library を使い、ページをレンダリングして結果の HTML を確認します。データ取得部分はモック化します。

  • Client Components のテスト
    従来の React コンポーネントと同様に、React Testing Library や Jest、Vitest を使って、ユーザーインタラクション(クリック、入力など)に基づいたテストを行います。

  • E2E テスト
    Cypress や Playwright などのツールを使い、実際のブラウザ環境でユーザーシナリオ全体をテストすることも重要です。これにより、Server/Client コンポーネント間の連携やナビゲーションが正しく機能することを確認できます。

7. まとめと実践への道筋

7.1 習得したスキルの整理

本記事を通じて、Next.js App Router の核心的な概念をマスターしました。

  • コンポーネントの使い分け
    Server Components を基本とし、インタラクティブ性が必要な箇所だけを Client Components として切り出すという、App Router の基本設計思想を理解しました。
  • 最新のデータフェッチング
    async/awaitを使った直感的なサーバーサイドでのデータ取得、Next.js 15 における新しいキャッシュ戦略、そして SWR などを活用したクライアントサイドでの効率的なデータ取得方法を学びました。
  • UI の状態管理
    loading.tsxerror.tsx、そして Suspense を活用し、ユーザー体験を向上させるための宣言的な UI 状態管理手法を習得しました。
  • 実践的な開発パターン
    ナビゲーション、動的メタデータ生成、reactcache関数を使ったデータ取得の最適化など、実務で役立つテクニックを身につけました。

7.2 実際のプロジェクトでの活用

これらの知識を武器に、ぜひ実際のプロジェクトで App Router を試してみてください。

  • 小さなコンポーネントから始める
    既存の Pages Router プロジェクトに、新しいページを App Router で作成してみるのが良い第一歩です。
  • キャッシュ戦略を意識する
    新規プロジェクトでは、Next.js 15 のキャッシュ動作を前提に、データの特性に合わせたキャッシュ戦略を設計しましょう。
  • チームでの認識合わせ
    チームで開発する場合、Server/Client コンポーネントの境界線をどこに引くか、共通のルールを設けることが重要です。

7.3 Next.js 15 への移行における注意点

Next.js 14 以前から移行する場合、最も注意すべきはfetchのキャッシュ動作の変更です。これまでデフォルトでキャッシュされていたものが、キャッシュされなくなるため、意図しないパフォーマンス低下を招く可能性があります。移行時には、データ取得箇所をレビューし、必要に応じてcache: 'force-cache'next: { revalidate: ... }を明示的に追加してください。

7.4 さらなる学習への道筋

App Router の世界はさらに奥深く、探求すべきトピックは尽きません。

  • Server Actions
    フォームの送信などを、クライアントサイドの JavaScript なしで実現する強力な機能です。本シリーズの次回で詳しく解説します。
  • 高度なキャッシュ戦略
    revalidateTagrevalidatePathを使った、より動的なキャッシュの無効化。
  • 大規模アプリケーション設計
    ルートグループやプライベートフォルダを活用した、複雑なアプリケーションの構造設計。

第3回の記事は以下からご覧いただけます。
Next.js App Router の基礎:フロントエンドエンジニアのための入門ガイド(Server Actionsとフォーム処理編)

7.5 参考資料とコミュニティ

公式ドキュメントは常に最新の情報源です。また、Discord などのコミュニティに参加し、他の開発者と情報交換することも、スキルアップの近道です。

App Router は、モダンな Web 開発の新しいスタンダードを提示しています。今回学んだ知識を土台に、ぜひあなたの手で、高速でリッチなユーザー体験を持つアプリケーションを構築してみてください。


hiraokuのプロフィール画像
hiraoku

暇があったらクライミングしているフロントエンドエンジニアです。

シナジーマーケティング株式会社では一緒に働く仲間を募集しています。