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として扱われます:
import { useState } from "react";
export default function Card() {
const [state, setState] = useState(""); // エラー発生箇所
}
Server Componentsの特徴:
- ブラウザで実行されるJavaScriptを生成しない
- イベントハンドラや
useState
などのクライアントサイド機能が使用不可 - パフォーマンス最適化のためサーバーサイドでのみ実行
なぜこの設計か?
Server Componentsは:
- クライアントに送信されるJSバンドルサイズを削減
- データベース接続などサーバーリソースを直接利用可能
- PHP/Railsのような開発体験をReactで実現
解決策:use client
ディレクティブ
コンポーネントがクライアント機能を必要とする場合、ファイルの先頭に'use client'
ディレクティブを追加します:
'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>
);
}
ポイント
- ディレクティブはファイル最上部に記述(コメントよりも上)
- import文の前に配置
- 変更後、開発サーバーを再起動
サーバー vs クライアントコンポーネントの使い分け
ケース | 推奨コンポーネント |
---|---|
インタラクティブUI(ボタン等) | クライアント |
静的なコンテンツ表示 | サーバー |
データフェッチ | サーバー |
useState /useEffect が必要 | クライアント |
ブラウザAPI利用 | クライアント |
応用パターン
サードパーティライブラリの扱い
クライアント機能を持つライブラリをサーバーコンポーネントで使用する場合:
'use client';
export * from '@mui/material';
import { Button } from '../lib/mui'; // ラッパー経由でインポート
export default function Page() {
return <Button variant="contained">送信</Button>;
}
コンテキストプロバイダーの実装
グローバル状態管理が必要な場合:
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext('');
export default function ThemeProvider({ children }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
);
}
import ThemeProvider from './theme-provider';
export default function Layout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
高度な注意点
サーバーコンポーネント間での状態共有
誤ったアプローチ
クライアントストアをサーバーコンポーネント間で共有しようとするとエラー発生
正しい解決法:
// グローバルシングルトンパターン
export const db = new DatabaseConnection();
// 各サーバーコンポーネントで直接インポート
import { db } from '@utils/database';
export async function UsersLayout() {
const users = await db.query(); // 直接呼び出し
}
データフェッチの最適化
複数コンポーネントで同じデータが必要な場合:
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()
呼び出しを自動で重複排除し、キャッシュします。
パフォーマンスのベストプラクティス
クライアントコンポーネントの最小化
- インタラクティブ要素のみを分離
js'use client'; // 小さなクライアントコンポーネント
サーバーコンポーネントでデータ処理
js// サーバーコンポーネント内 const data = await getData(); // DB/APIから直接取得 return <ClientComponent processedData={preprocess(data)} />;
ツリー構造の最適化
app/ ├── layout.js // サーバーコンポーネント ├── page.js // サーバーコンポーネント └── components/ ├── server-comp.js └── client/ // クライアントコンポーネント専用ディレクトリ └── interactive.js
Server Componentsを適切に活用することで、アプリケーションのパフォーマンスとユーザー体験を大幅に向上させることが可能です。