サーバー/クライアントコンポーネントを理解する
Next.js App Routerでは、すべてのコンポーネントはデフォルトでサーバーコンポーネントです。 しかし、時にはクライアント側の機能(例:useState、onClickなど)が必要な場合があります。このような場合はどうすればよいでしょうか?
クライアントコンポーネントを使用するには、ファイルの先頭に'use client'
ディレクティブを追加します。
'use client'
export const ClientComponent = () => {
return (
<div>
<h1>Client Component</h1>
</div>
);
};
これにより、そのコンポーネントからクライアント境界(Client Boundary)が形成され、コンポーネントでインポートされるすべてのコンポーネントおよびモジュールファイルはクライアント側で動作します。 (ここで重要なのは、コンポーネントのレンダリング階層構造ではなく、インポート関係です。)
そのため、'use client'
を付けていないコンポーネントも自動的にクライアントコンポーネントに変換されます。
このような場合はどうでしょうか?クライアントコンポーネント内にサーバーコンポーネントを宣言してみましょう。
import React from 'react';
export const ServerComponent = async () => {
const data = await getData();
return <div>{JSON.stringify(data)}</div>;
};
async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const json = await res.json();
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return json;
}
export default ServerComponent;
'use client'
import ServerComponent from './ServerComponent'
export const ClientComponent = () => {
return (
<div>
<h1>Client Component</h1>
<ServerComponent />
</div>
)
}
次のような警告とともにエラーログが表示されます:
クライアントコンポーネントとサーバーコンポーネントを一緒に使用する際には、以下のような重要なルールがあります:
- ✅ サーバーコンポーネントの下にクライアントコンポーネント(可能)
- ✅ クライアントコンポーネントの下にクライアントコンポーネント(可能)
- ❌ クライアントコンポーネントの下にサーバーコンポーネント(不可能)
🔧 解決策:Propsパターンの活用
では、クライアントコンポーネント内でサーバーコンポーネントを使用するにはどうすればよいでしょうか?答えは簡単です:propsとして渡します。
// 動作するパターン
'use client'
export const ClientComponent = ({ children }) => {
return (
<div>
<h1>Client Component</h1>
{children}
</div>
);
};
const App = () => {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
なぜこのように動作するのか?
鍵となるのは、Client Boundaryがコンポーネントの親子関係ではなく、インポート位置を基準に決定されるという点です。
上記の例では、ServerComponent
はサーバーコンポーネントであるAppからインポートされているため、サーバーコンポーネントとして維持されます。
ClientComponent
は単にpropsとして受け取ったchildrenがどこでレンダリングされるかを知っているだけで、どのようなコンポーネントが来るかは知りません。
このパターンにより、Layoutにuse clientで宣言されたProviderなどで包まれていても、サーバーコンポーネントを使用することができます。
詳細な解説
サーバーコンポーネントは、従来のサーバーサイドレンダリング(SSR)とは全く異なる方法で動作します。
SSRがサーバーで完成したHTMLを生成するのに対し、サーバーコンポーネントはReact Server Components(RSC)Payloadと呼ばれる特別な形式のJSON-likeストリームを生成します。
このRSC Payloadには、コンポーネントツリー構造、データ、HTMLコンテンツなどが含まれており、サーバーでは文字列、数値、配列などの基本的なデータ型とReactエレメント、Dateオブジェクト、Map、Setなどの組み込みオブジェクトをシリアライズしてこのペイロードに含めます。 一方、関数、クラスインスタンス、クロージャ、イベントリスナーなどはシリアライズできません。
したがって、サーバーコンポーネントからクライアントコンポーネントにシリアライズできない値をprops
として渡すことはできません!
サーバーコンポーネントをレンダリングする過程でクライアントコンポーネントに遭遇すると、その部分は特別なプレースホルダーでマークされます。 このプレースホルダーにはクライアントコンポーネントの参照とprops情報が含まれており、クライアントで該当コンポーネントを正確にレンダリングできるようになっています。
クライアントはサーバーから受け取ったRSCペイロードをReactが理解できる形式に変換し、サーバーから受け取ったコンテンツをハイドレーションします。 このとき、プレースホルダーでマークされた部分には該当するクライアントコンポーネントがレンダリングされます。
参照