さとまたプログラミングラボ

データ取得

Server Componentsでのデータフェッチング

サーバーコンポーネントでのfetch

async コンポーネント

サーバー上で直接データ取得

typescript
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
プレビュー
// Server Component なので
// - APIキーが漏れない
// - DBに直接アクセス可能
// - 初回表示が速い

キャッシュとrevalidate

キャッシュの制御

データの鮮度を管理

typescript
// デフォルト: キャッシュされる
const res = await fetch('https://...');

// キャッシュしない(毎回リクエスト)
const res = await fetch('https://...', {
  cache: 'no-store'
});

// 一定時間後に再検証
const res = await fetch('https://...', {
  next: { revalidate: 60 }  // 60秒後に再検証
});

// ページ全体のrevalidate
export const revalidate = 60;
プレビュー
'force-cache' // キャッシュ優先
'no-store' // 常に新しいデータ
revalidate: 60 // 60秒ごと更新

並列データ取得

Promise.all で高速化

複数のリクエストを同時実行

typescript
// app/dashboard/page.tsx
async function getUser() {
  const res = await fetch('https://api.example.com/user');
  return res.json();
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

export default async function Dashboard() {
  // 並列で取得(速い!)
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ]);

  return (
    <div>
      <h1>{user.name}さんのダッシュボード</h1>
      <PostList posts={posts} />
    </div>
  );
}
プレビュー
// 順番に取得: 2秒 + 2秒 = 4秒
// 並列で取得: max(2秒, 2秒) = 2秒

動的パラメータを使った取得

paramsを使ったデータ取得

URLパラメータでAPIを呼び出す

typescript
// app/blog/[slug]/page.tsx
interface Props {
  params: { slug: string }
}

async function getPost(slug: string) {
  const res = await fetch(
    `https://api.example.com/posts/${slug}`
  );

  if (!res.ok) {
    notFound();  // 404ページを表示
  }

  return res.json();
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}
プレビュー
/blog/hello-world
// → getPost("hello-world")
// → API: /posts/hello-world

ローディング状態

loading.tsx

自動的にローディングUIを表示

typescript
// app/posts/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
      <div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
      <div className="h-4 bg-gray-200 rounded w-5/6"></div>
    </div>
  );
}

// app/posts/page.tsx はそのまま
// データ取得中は自動的にloading.tsxが表示される
プレビュー

Suspenseで部分的にローディング

コンポーネント単位でローディングを制御

typescript
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>ダッシュボード</h1>

      {/* このコンポーネントだけローディング */}
      <Suspense fallback={<p>投稿を読み込み中...</p>}>
        <PostList />
      </Suspense>

      {/* これはすぐ表示される */}
      <Sidebar />
    </div>
  );
}
プレビュー
// Suspenseで囲んだ部分だけ
// 待っている間 fallback を表示

エラーハンドリング

error.tsx

エラー時のフォールバックUI

typescript
// app/posts/error.tsx
'use client';

interface Props {
  error: Error;
  reset: () => void;
}

export default function Error({ error, reset }: Props) {
  return (
    <div>
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>
        もう一度試す
      </button>
    </div>
  );
}
プレビュー

エラーが発生しました

データの取得に失敗しました