Documentation Next.js

はじめに

この記事では、Next.js 13で導入されたServer ComponentsとClient Componentsの違いと使い分けについて解説します。Server Componentsとは、サーバー側でレンダリングされるReactコンポーネントのことで、クライアントにJavaScriptを送信せずにHTMLを生成できます。一方、Client Componentsは、ブラウザ上で実行されるコンポーネントで、ユーザーとのインタラクションや状態管理を担当します。

Server ComponentsとClient Componentsの比較

特徴Server ComponentsClient Components
実行場所サーバーブラウザ
JavaScript送信なしあり
データ取得直接可能(async/await)useEffect/SWR等が必要
状態管理不可useState等で可能
イベントハンドラ不可onClick等で可能
ブラウザAPI不可window/document等使用可能
デフォルトデフォルト'use client'が必要

Server Componentsの利点

パフォーマンスの最適化

Server Componentsは、サーバーでレンダリングされたHTMLを直接クライアントに送信するため、クライアントに送るJavaScriptを減らすことができます。これにより、ページの初期読み込み速度が向上し、ユーザー体験が改善されます。

// app/products/page.tsx(Server Component)
// async/awaitで直接データ取得可能
export default async function ProductsPage() {
  // サーバーで実行されるため、APIキーは安全
  const products = await fetch('https://api.example.com/products', {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
    next: { revalidate: 3600 }, // 1時間キャッシュ
  }).then((res) => res.json());

  return (
    <main>
      <h1>商品一覧</h1>
      <ul>
        {products.map((product: { id: string; name: string; price: number }) => (
          <li key={product.id}>
            {product.name} - ¥{product.price.toLocaleString()}
          </li>
        ))}
      </ul>
    </main>
  );
}

SEO向上

サーバーでレンダリングされたHTMLは、検索エンジンに対して容易にクロールされやすく、SEOの向上にも寄与します。

バンドルサイズの削減

Server Componentsで使用するライブラリ(マークダウンパーサー、シンタックスハイライターなど)はクライアントに送信されないため、バンドルサイズを大幅に削減できます。

// app/blog/[slug]/page.tsx
import { marked } from 'marked'; // このライブラリはクライアントに送信されない
import hljs from 'highlight.js';

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  const html = marked(post.content);

  return (
    <article dangerouslySetInnerHTML={{ __html: html }} />
  );
}

Client Componentsの利点

ユーザーインタラクションの処理

Client Componentsは、ユーザーとのインタラクションや動的な状態管理を担います。

// components/AddToCartButton.tsx
'use client';

import { useState } from 'react';

interface Props {
  productId: string;
  productName: string;
}

export default function AddToCartButton({ productId, productName }: Props) {
  const [isAdding, setIsAdding] = useState(false);
  const [isAdded, setIsAdded] = useState(false);

  const handleAddToCart = async () => {
    setIsAdding(true);
    try {
      await fetch('/api/cart', {
        method: 'POST',
        body: JSON.stringify({ productId }),
      });
      setIsAdded(true);
    } finally {
      setIsAdding(false);
    }
  };

  return (
    <button
      onClick={handleAddToCart}
      disabled={isAdding || isAdded}
      className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
    >
      {isAdded ? '追加済み' : isAdding ? '追加中...' : 'カートに追加'}
    </button>
  );
}

ブラウザAPIの使用

windowdocumentなどのブラウザAPIにアクセスする必要がある場合に使用します。

// components/ScrollToTop.tsx
'use client';

import { useEffect, useState } from 'react';

export default function ScrollToTop() {
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const toggleVisibility = () => {
      setIsVisible(window.scrollY > 300);
    };

    window.addEventListener('scroll', toggleVisibility);
    return () => window.removeEventListener('scroll', toggleVisibility);
  }, []);

  const scrollToTop = () => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  if (!isVisible) return null;

  return (
    <button
      onClick={scrollToTop}
      className="fixed bottom-4 right-4 p-3 bg-gray-800 text-white rounded-full"
      aria-label="トップへ戻る"
    >

    </button>
  );
}

コンポーネントの合成パターン

Server ComponentをClient Componentの子として渡す

Server ComponentをClient Componentのchildrenとして渡すことで、Server Componentの利点を維持できます。

// components/Sidebar.tsx(Client Component)
'use client';

import { useState } from 'react';

interface Props {
  children: React.ReactNode;
}

export default function Sidebar({ children }: Props) {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <aside className={isOpen ? 'w-64' : 'w-0'}>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? '閉じる' : '開く'}
      </button>
      {isOpen && children}
    </aside>
  );
}
// app/layout.tsx(Server Component)
import Sidebar from '@/components/Sidebar';
import Navigation from '@/components/Navigation'; // Server Component

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar>
        {/* NavigationはServer Componentのまま */}
        <Navigation />
      </Sidebar>
      <main>{children}</main>
    </div>
  );
}

インタラクティブな部分だけをClient Componentに分離

// app/products/[id]/page.tsx(Server Component)
import AddToCartButton from '@/components/AddToCartButton';
import ProductReviews from '@/components/ProductReviews';

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await getProduct(params.id);

  return (
    <div>
      {/* 静的な部分はServer Component */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p className="text-2xl font-bold">
        ¥{product.price.toLocaleString()}
      </p>

      {/* インタラクティブな部分だけClient Component */}
      <AddToCartButton
        productId={product.id}
        productName={product.name}
      />

      {/* レビューはServer Componentで取得 */}
      <ProductReviews productId={product.id} />
    </div>
  );
}

Suspenseによるストリーミング

Server ComponentsとSuspenseを組み合わせることで、UIを段階的にストリーミングできます。

// app/dashboard/page.tsx
import { Suspense } from 'react';
import UserStats from '@/components/UserStats';
import RecentActivity from '@/components/RecentActivity';
import Notifications from '@/components/Notifications';

export default function Dashboard() {
  return (
    <div className="grid grid-cols-3 gap-4">
      <Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
        <UserStats />
      </Suspense>

      <Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
        <RecentActivity />
      </Suspense>

      <Suspense fallback={<div className="animate-pulse h-32 bg-gray-200" />}>
        <Notifications />
      </Suspense>
    </div>
  );
}
// components/UserStats.tsx(Server Component)
export default async function UserStats() {
  // このデータ取得が完了次第、このコンポーネントがストリーミングされる
  const stats = await fetchUserStats();

  return (
    <div className="p-4 bg-white rounded shadow">
      <h2>統計情報</h2>
      <p>投稿数: {stats.posts}</p>
      <p>フォロワー: {stats.followers}</p>
    </div>
  );
}

使い分けの判断フロー

コンポーネントを作成する

    ├── useState/useEffect/useReducer を使う?
    │   └── Yes → Client Component

    ├── onClick/onChange などのイベントハンドラを使う?
    │   └── Yes → Client Component

    ├── window/document などのブラウザAPIを使う?
    │   └── Yes → Client Component

    ├── React Context の Provider を作成する?
    │   └── Yes → Client Component

    └── 上記すべて No
        └── Server Component(デフォルト)

よくある間違いと対処法

間違い1: Server ComponentでuseStateを使う

// NG: Server ComponentでuseStateは使えない
export default function Counter() {
  const [count, setCount] = useState(0); // エラー
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// OK: 'use client'を追加
'use client';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

間違い2: Client ComponentからServer Componentをimport

// NG: Client ComponentからServer Componentを直接import
'use client';

import ServerComponent from './ServerComponent'; // 問題あり

export default function ClientWrapper() {
  return <ServerComponent />; // Server Componentとして動作しない
}
// OK: childrenとして渡す
'use client';

export default function ClientWrapper({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div className="wrapper">{children}</div>;
}

// 親コンポーネント(Server Component)で
<ClientWrapper>
  <ServerComponent />
</ClientWrapper>

間違い3: 不必要にClient Componentにする

// NG: 静的な表示だけなのにClient Component
'use client';

export default function Footer() {
  return (
    <footer>
      <p>© 2024 Example Inc.</p>
    </footer>
  );
}
// OK: 'use client'を削除(Server Componentで十分)
export default function Footer() {
  return (
    <footer>
      <p>© 2024 Example Inc.</p>
    </footer>
  );
}

Server Actions(簡易紹介)

Next.js 14以降では、Server Actionsを使ってClient Componentから直接サーバー関数を呼び出せます。

// app/actions.ts
'use server';

export async function submitForm(formData: FormData) {
  const name = formData.get('name');
  await saveToDatabase({ name });
  return { success: true };
}
// components/ContactForm.tsx
'use client';

import { submitForm } from '@/app/actions';

export default function ContactForm() {
  return (
    <form action={submitForm}>
      <input name="name" placeholder="お名前" />
      <button type="submit">送信</button>
    </form>
  );
}

まとめ

Next.js 13以降で導入されたServer ComponentsとClient Componentsを適切に使い分けることで、パフォーマンスとユーザーエクスペリエンスを大幅に向上させることができます。

  • Server Components: データ取得、静的コンテンツ、SEO重視のページ
  • Client Components: ユーザーインタラクション、状態管理、ブラウザAPI

デフォルトはServer Componentなので、インタラクティブな機能が必要な部分だけを'use client'でClient Componentにする設計が推奨されます。

参考文献

円