Skip to content

Server ComponentsとClient Componentsの区別

Next.jsのappディレクトリで作業中にuseStateを使用すると、次のエラーが発生することがあります:

× You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.

このエラーは、Server Components環境でクライアント専用機能を使用した場合に発生します。本記事では問題の本質と解決策を解説します。


問題の本質

Next.jsのappディレクトリでは、デフォルトですべてのコンポーネントがServer Componentsとして扱われます:

js
import { useState } from "react";

export default function Card() {
  const [state, setState] = useState(""); // エラー発生箇所
}

Server Componentsの特徴:

  • ブラウザで実行されるJavaScriptを生成しない
  • イベントハンドラやuseStateなどのクライアントサイド機能が使用不可
  • パフォーマンス最適化のためサーバーサイドでのみ実行

なぜこの設計か?

Server Componentsは:

  1. クライアントに送信されるJSバンドルサイズを削減
  2. データベース接続などサーバーリソースを直接利用可能
  3. PHP/Railsのような開発体験をReactで実現

解決策:use clientディレクティブ

コンポーネントがクライアント機能を必要とする場合、ファイルの先頭'use client'ディレクティブを追加します:

js
'use client'; // クライアントコンポーネント宣言

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>クリック回数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        カウントアップ
      </button>
    </div>
  );
}

ポイント

  1. ディレクティブはファイル最上部に記述(コメントよりも上)
  2. import文の前に配置
  3. 変更後、開発サーバーを再起動

サーバー vs クライアントコンポーネントの使い分け

ケース推奨コンポーネント
インタラクティブUI(ボタン等)クライアント
静的なコンテンツ表示サーバー
データフェッチサーバー
useState/useEffectが必要クライアント
ブラウザAPI利用クライアント

応用パターン

サードパーティライブラリの扱い

クライアント機能を持つライブラリをサーバーコンポーネントで使用する場合:

js
'use client';

export * from '@mui/material';
js
import { Button } from '../lib/mui'; // ラッパー経由でインポート

export default function Page() {
  return <Button variant="contained">送信</Button>;
}

コンテキストプロバイダーの実装

グローバル状態管理が必要な場合:

js
'use client';

import { createContext } from 'react';

export const ThemeContext = createContext('');

export default function ThemeProvider({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  );
}
js
import ThemeProvider from './theme-provider';

export default function Layout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

高度な注意点

サーバーコンポーネント間での状態共有

誤ったアプローチ

クライアントストアをサーバーコンポーネント間で共有しようとするとエラー発生

正しい解決法

js
// グローバルシングルトンパターン
export const db = new DatabaseConnection();
js
// 各サーバーコンポーネントで直接インポート
import { db } from '@utils/database';

export async function UsersLayout() {
  const users = await db.query(); // 直接呼び出し
}

データフェッチの最適化

複数コンポーネントで同じデータが必要な場合:

js
export default async function Layout({ children }) {
  const data = await fetch('https://api.example.com/data'); // サーバーで一度のみ実行
  return <ClientComponent data={data}>{children}</ClientComponent>;
}

Next.jsは同一URLへのfetch()呼び出しを自動で重複排除し、キャッシュします。


パフォーマンスのベストプラクティス

  1. クライアントコンポーネントの最小化

    • インタラクティブ要素のみを分離
    js
    'use client';
    // 小さなクライアントコンポーネント
  2. サーバーコンポーネントでデータ処理

    js
    // サーバーコンポーネント内
    const data = await getData(); // DB/APIから直接取得
    return <ClientComponent processedData={preprocess(data)} />;
  3. ツリー構造の最適化

    app/
    ├── layout.js        // サーバーコンポーネント
    ├── page.js          // サーバーコンポーネント
    └── components/
        ├── server-comp.js
        └── client/      // クライアントコンポーネント専用ディレクトリ
            └── interactive.js

Server Componentsを適切に活用することで、アプリケーションのパフォーマンスとユーザー体験を大幅に向上させることが可能です。