💭

さよなら getServerSideProps、こんにちは async/await コンポーネント

に公開

さよなら getServerSideProps、こんにちは async/await コンポーネント

Next.js 16(App Router)への移行は、単なる記述の変更ではありません。
データの「バケツリレー」を終わらせ、コンポーネントを自立させるアーキテクチャの進化です。


1. 最大のパラダイムシフト:Push型からPull型へ

これまでの Pages Router では、ページ最上部の getServerSideProps ですべてのデータを取得し、Props を通じて下流へ「Push」していました。これが「バケツリレー」の正体です。

Pages Router(Push型)

  • ページトップで全データを抱え込む
  • 子・孫へ Props で流し込む

Next.js 16(Pull型)

  • 必要なコンポーネントが、必要な場所で、必要なデータを直接 await する。

2. 実践:Layout と Container の役割分担(境界線)

getServerSideProps を解体する際に最も重要なのが「どこで await するか」の判断基準です。
ポイントは URLとの連動性 にあります。

取得場所 Layout(枠) Container(主役)
データの性質 共通・普遍的 固有・具体的
URLとの関係 ページ遷移しても維持される URLが変わるたびに更新される
API例 getCategories(), getMe() getDiaryDetail(id), getMemos(q)

3. コードで見るリファクタリング

【Before】 Pages Router:巨大な Props バケツ

ページトップの関数が、すべてのコンポーネントのデータ依存を知っている必要がありました。

// pages/diary/[id].tsx
export const getServerSideProps = async (ctx) => {
  const [diary, categories] = await Promise.all([
    getDiary(ctx.params.id),
    getCategories()
  ]);

  return { props: { diary, categories } };
};

export default function Page({ diary, categories }) {
  return (
    <Layout categories={categories}>
      <DiaryContent diary={diary} />
    </Layout>
  );
}

【After】 Next.js 16:自立したコンポーネント

各階層が「自分に必要なデータ」だけを取得します。

// app/admin/diary/[id]/layout.tsx (Server Component)
export default async function Layout({ children }) {
  // 共通のサイドバー用データのみ取得
  const categories = await getCategories();
  return <Sidebar categories={categories}>{children}</Sidebar>;
}

// features/diary/components/DiaryContainer.tsx (Server Component)
export async function DiaryContainer({ id }) {
  // そのページ固有のメインデータのみ取得
  const diary = await getDiaryDetail(id);
  
  return (
    <article>
      <h1>{diary.title}</h1>
      <p>{diary.content}</p>
      {/* インタラクションが必要な島(Island)だけ Client Component を呼ぶ */}
      <DiaryActions diaryId={id} />
    </article>
  );
}

4. なぜ「重複通信」を心配しなくていいのか?

ITエンジニアが懸念する「Layout と Page で同じ API を叩いたら二重リクエストになる」問題。Next.js 16 はこれを Request Memoization で解決しました。

  • 同一レンダリング内
  • 同一の fetch(URL + options)

であれば、自動的に 1 回にまとめられます。エンジニアは「誰かが取得済みか」を気にする必要はなく、「このコンポーネントを表示するのにこのデータが必要だ」と思ったら、迷わず await すればいいのです。

※注:headerscache 設定が異なる場合は別リクエストとして扱われます。


5. クライアントコンポーネント(動き)の連携例

サーバーがデータを await し、クライアントが「動き(Interaction)」を担当します。

// features/diary/components/DiaryActions.tsx
'use client';

import { useRouter } from 'next/navigation';

export function DiaryActions({ diaryId }) {
  const router = useRouter();

  const handleLike = async () => {
    await updateLike(diaryId);

    // ★重要:router.refresh() を呼ぶと、
    // Layoutを含むサーバーコンポーネントのみが再実行され、画面が最新化される
    router.refresh();
  };

  return <button onClick={handleLike}>❤️ いいね</button>;
}

6. エンジニアが得る「自由」

  • バケツリレーの廃止: Props の受け渡し地獄から解放され、コンポーネントの配置変更や再利用が容易になります。
  • Streaming UX: Layout は先に表示。重いデータ(Container)は Suspenseloading.tsx で待つ。これが今の最適解です。
  • 真のカプセル化: 「その機能のデータ取得は、その機能の Container を見ればいい」という直感的な開発が実現します。

結論

getServerSideProps を捨てることは、単なる書き方の変更ではなく、設計思想の進化です。

ルールはシンプル。

  • 共通データ = Layout
  • 固有データ = Container
  • 動き = Client

これだけ守れば、あなたの Next.js アプリは劇的にシンプルで、保守性の高い構造になります。

Discussion