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