React, Vue.js
投稿日:

AI駆動開発でReact Server Componentsのデータ取得パターンを設計する

こんにちは、株式会社ファストコーディングのフルスタックエンジニア、独身貴族Fireです。

先週末、ロックフェスのチケットを2枚取ったんです。友人と行く予定だったのですが、友人が「当日の天気を見てから決めたい」と。それなら先に会場のアクセスだけ調べておいて、天気予報は当日朝に確認しよう、と段取りを分けました。

データ取得も同じです。全部一度に取る必要はない。先に取れるものと後で取ればいいものを分けるのが設計の基本です。

AI駆動開発でNext.js App RouterのReact Server Components(RSC)におけるデータ取得パターンを設計しました。AIは基本的なfetchパターンを提案しますが、「何を並列で取得して何を直列にするか」「Server ComponentとClient Componentのどちらでデータを持つか」の判断には、画面の要件に対する理解が必要です。

Server Componentsでのデータ取得の基本

Next.js App RouterのServer Componentsでは、コンポーネント内で直接async/awaitを使ってデータを取得できます。APIルート経由ではなく、コンポーネント自体が非同期関数になるのが特徴です。

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`, {
    next: { revalidate: 3600 },
  });
  if (!res.ok) throw new Error('Failed to fetch user');
  return res.json();
}

export default async function UserPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const user = await getUser(id);
  return <div>{user.name}</div>;
}

next: { revalidate: 3600 }はISR(Incremental Static Regeneration)の設定です。1時間キャッシュして、次のリクエストでバックグラウンドで再検証します。

AIに設計を依頼する:ダッシュボード画面

管理画面のダッシュボードを例に、AIにデータ取得パターンの設計を依頼しました。

セクションデータ更新頻度
ヘッダーログインユーザー情報ほぼ変わらない
KPIカード売上・注文数・ユーザー数1時間おき
最近の注文直近10件の注文一覧リアルタイムに近い
お知らせ運営からのお知らせ1日1回

AIの提案:全部直列で取得

AIの最初の提案は4つのAPI呼び出しが直列で実行されるコードでした。各APIが200msかかるとすると合計800ms。ユーザーは800ms待たないと画面が表示されません。

修正1:並列取得に変更

独立したデータソースはPromise.allで並列取得にします。

export default async function DashboardPage() {
  const [user, kpi, orders, notices] = await Promise.all([
    getUser(),
    getKPI(),
    getRecentOrders(),
    getNotices(),
  ]);

  return (
    <div>
      <Header user={user} />
      <KPICards kpi={kpi} />
      <RecentOrders orders={orders} />
      <Notices notices={notices} />
    </div>
  );
}

4つのAPIが並列で実行されるため、合計の待ち時間は最も遅いAPIの時間(200ms)になります。800msから200msへ、75%の改善です。

ただし、Promise.allは1つでも失敗すると全体が失敗します。4つのセクションのうち1つだけAPIエラーが起きた場合、画面全体が表示されなくなる問題があります。

修正2:SuspenseとストリーミングSSR

Next.js App Routerでは、Suspenseを使ってコンポーネント単位で段階的に表示できます。各セクションを独立したServer Componentにして、Suspenseで囲みます。

// app/dashboard/page.tsx — Suspense + ストリーミング
import { Suspense } from 'react';

export default async function DashboardPage() {
  return (
    <div className="p-6 space-y-6">
      <Suspense fallback={<HeaderSkeleton />}>
        <HeaderSection />
      </Suspense>
      <Suspense fallback={<KPISkeleton />}>
        <KPISection />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrdersSection />
      </Suspense>
      <Suspense fallback={<NoticesSkeleton />}>
        <NoticesSection />
      </Suspense>
    </div>
  );
}

async function HeaderSection() {
  const user = await getUser();
  return <Header user={user} />;
}

async function KPISection() {
  const kpi = await getKPI();
  return <KPICards kpi={kpi} />;
}

async function RecentOrdersSection() {
  const orders = await getRecentOrders();
  return <RecentOrders orders={orders} />;
}

async function NoticesSection() {
  const notices = await getNotices();
  return <Notices notices={notices} />;
}

このパターンのメリットは3つです。

  1. 段階的な表示。データが返ってきたセクションから順に表示される。ユーザーはスケルトンUIを見ながら待てる
  2. エラーの局所化。1つのAPIが失敗しても、他のセクションは表示される。各Suspenseの中にerror.tsxを配置すれば、セクション単位でエラーハンドリングできる
  3. 並列実行。独立したSuspenseバウンダリで囲まれた各コンポーネントのデータ取得は、互いにブロックせず並列に実行される

キャッシュ戦略の設計

AIの提案にはキャッシュ戦略が含まれていませんでした。データの更新頻度に応じてキャッシュ時間を変えるべきです。

// データの更新頻度に応じたキャッシュ設定
async function getUser() {
  const res = await fetch('https://api.example.com/user', {
    next: { revalidate: 3600 },  // 1時間キャッシュ
  });
  if (!res.ok) throw new Error('Failed to fetch user');
  return res.json();
}

async function getKPI() {
  const res = await fetch('https://api.example.com/kpi', {
    next: { revalidate: 3600 },  // 1時間キャッシュ
  });
  if (!res.ok) throw new Error('Failed to fetch KPI');
  return res.json();
}

async function getRecentOrders() {
  const res = await fetch('https://api.example.com/orders/recent', {
    cache: 'no-store',  // キャッシュしない(リアルタイム)
  });
  if (!res.ok) throw new Error('Failed to fetch orders');
  return res.json();
}

async function getNotices() {
  const res = await fetch('https://api.example.com/notices', {
    next: { revalidate: 86400 },  // 24時間キャッシュ
  });
  if (!res.ok) throw new Error('Failed to fetch notices');
  return res.json();
}
データキャッシュ設定理由
ユーザー情報revalidate: 3600プロフィール変更は少ない
KPIrevalidate: 36001時間ごとの集計で十分
最近の注文cache: ‘no-store’リアルタイムに近い情報が必要
お知らせrevalidate: 864001日1回の更新で十分

なお、Next.js 15ではfetchのデフォルトがno-store(キャッシュなし)に変更されています。そのため、キャッシュしたいデータには明示的にrevalidateを指定する必要があります。AIは全てのfetchに同じキャッシュ設定を適用する傾向がありますが、画面の要件に応じてデータごとにキャッシュ時間を変えるのは、ビジネスロジックを理解している人間の仕事です。

Server ComponentとClient Componentの境界

AIが見落としていた重要なポイントの1つが、Server ComponentとClient Componentの境界設計です。「最近の注文」セクションにリアルタイムで自動更新する機能をつけたい場合、Server Componentはリクエスト時に1回データを取得するだけで、ブラウザ側での自動更新はできません。

最初のデータをServer Componentで取得し、Client Componentに渡して自動更新させるパターンが効果的です。

// Server Component:初回データを取得してClient Componentに渡す
async function RecentOrdersSection() {
  const initialOrders = await getRecentOrders();
  return <RecentOrdersClient initialOrders={initialOrders} />;
}
// RecentOrdersClient.tsx
'use client';

import { useState, useEffect } from 'react';

type Order = {
  id: string;
  amount: number;
  status: string;
  createdAt: string;
};

export function RecentOrdersClient({
  initialOrders,
}: {
  initialOrders: Order[];
}) {
  const [orders, setOrders] = useState(initialOrders);

  useEffect(() => {
    const interval = setInterval(async () => {
      const res = await fetch('/api/orders/recent');
      if (res.ok) {
        const data = await res.json();
        setOrders(data);
      }
    }, 30000);  // 30秒ごとに更新

    return () => clearInterval(interval);
  }, []);

  return (
    <div>
      <h2 className="text-lg font-bold mb-4">最近の注文</h2>
      <table className="w-full">
        <thead>
          <tr className="border-b">
            <th className="text-left p-2">注文ID</th>
            <th className="text-left p-2">金額</th>
            <th className="text-left p-2">ステータス</th>
          </tr>
        </thead>
        <tbody>
          {orders.map((order) => (
            <tr key={order.id} className="border-b">
              <td className="p-2">{order.id}</td>
              <td className="p-2">¥{order.amount.toLocaleString()}</td>
              <td className="p-2">{order.status}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

このパターンのメリットは以下の通りです。

  • 初回表示はSSR。Server Componentでデータを取得するため、初回のHTMLにはデータが含まれる。SEOにも有利
  • その後はCSR。Client Componentが30秒ごとにAPIを叩いてデータを更新する
  • プログレッシブエンハンスメント。JavaScriptが無効でも初回のデータは表示される

まとめ

今回は、AI駆動開発でReact Server Componentsのデータ取得パターンを設計する過程を紹介しました。

RSCのデータ取得設計でAIを活用する際のポイントは以下の3つです。

  1. 独立したデータはSuspenseで分割する。直列取得から並列取得に変えるだけで表示速度が大幅に改善する。Suspenseを使えばセクション単位でのストリーミング表示とエラーの局所化ができる
  2. キャッシュ戦略はデータごとに変える。Next.js 15ではfetchのデフォルトがキャッシュなしに変更されたため、キャッシュしたいデータには明示的にrevalidateを指定する
  3. Server → Client のデータの渡し方を設計する。初回はServer Componentで取得し、リアルタイム更新が必要な部分だけClient Componentに任せる

私たちの開発現場でも、AIにデータ取得のベースコードを生成させてから、キャッシュ戦略とSuspenseの境界を人間が調整するフローを採っています。データの取得方法自体はAIが得意ですが、「どの粒度で分割するか」「どのデータをいつキャッシュから落とすか」はビジネス要件を理解した人間が判断する領域です。

株式会社ファストコーディングでは、AI駆動開発を取り入れたNext.jsの設計・実装をサポートしています。「ページの初期表示が遅い」「データ取得の設計を見直したい」という方は、お問い合わせフォームからお気軽にご相談ください。