クライアントコンポーネント内でサーバーコンポーネントを使用する

profile image

Next.jsでは、すべてのコンポーネントはデフォルトでサーバーコンポーネントです。クライアントコンポーネント内でそれらを使用する方法を探ってみましょう。

この記事は DeepL によって翻訳されました。誤訳があれば教えてください!

サーバー/クライアントコンポーネントを理解する

Next.js App Routerでは、すべてのコンポーネントはデフォルトでサーバーコンポーネントです。 しかし、時にはクライアント側の機能(例:useState、onClickなど)が必要な場合があります。このような場合はどうすればよいでしょうか?

クライアントコンポーネントを使用するには、ファイルの先頭に'use client'ディレクティブを追加します。

jsx
'use client'

export const ClientComponent = () => {
 return (
   <div>
     <h1>Client Component</h1>
   </div>
 );
};

これにより、そのコンポーネントからクライアント境界(Client Boundary)が形成され、コンポーネントでインポートされるすべてのコンポーネントおよびモジュールファイルはクライアント側で動作します。 (ここで重要なのは、コンポーネントのレンダリング階層構造ではなく、インポート関係です。)

そのため、'use client'を付けていないコンポーネントも自動的にクライアントコンポーネントに変換されます。

このような場合はどうでしょうか?クライアントコンポーネント内にサーバーコンポーネントを宣言してみましょう。

jsx
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;
jsx
'use client'

import ServerComponent from './ServerComponent'

export const ClientComponent = () => {
  return (
    <div>
      <h1>Client Component</h1>
      <ServerComponent />
    </div>
  )
}

次のような警告とともにエラーログが表示されます: img01.png

クライアントコンポーネントとサーバーコンポーネントを一緒に使用する際には、以下のような重要なルールがあります:

  • ✅ サーバーコンポーネントの下にクライアントコンポーネント(可能)
  • ✅ クライアントコンポーネントの下にクライアントコンポーネント(可能)
  • ❌ クライアントコンポーネントの下にサーバーコンポーネント(不可能)

🔧 解決策:Propsパターンの活用

では、クライアントコンポーネント内でサーバーコンポーネントを使用するにはどうすればよいでしょうか?答えは簡単です:propsとして渡します。

jsx
// 動作するパターン
'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ストリームを生成します。 img02.png

このRSC Payloadには、コンポーネントツリー構造、データ、HTMLコンテンツなどが含まれており、サーバーでは文字列、数値、配列などの基本的なデータ型とReactエレメント、Dateオブジェクト、Map、Setなどの組み込みオブジェクトをシリアライズしてこのペイロードに含めます。 一方、関数、クラスインスタンス、クロージャ、イベントリスナーなどはシリアライズできません。

したがって、サーバーコンポーネントからクライアントコンポーネントにシリアライズできない値をpropsとして渡すことはできません!

サーバーコンポーネントをレンダリングする過程でクライアントコンポーネントに遭遇すると、その部分は特別なプレースホルダーでマークされます。 このプレースホルダーにはクライアントコンポーネントの参照とprops情報が含まれており、クライアントで該当コンポーネントを正確にレンダリングできるようになっています。

クライアントはサーバーから受け取ったRSCペイロードをReactが理解できる形式に変換し、サーバーから受け取ったコンテンツをハイドレーションします。 このとき、プレースホルダーでマークされた部分には該当するクライアントコンポーネントがレンダリングされます。


参照

❤️ 0
🔥 0
😎 0
⭐️ 0
🆒 0