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

🌐

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

サーバー/クライアントコンポーネントについて理解する

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
  )
}

すると、次のような警告文と一緒にエラーログが表示されます。 img01.png

このようにクライアントコンポーネントとサーバーコンポーネントを一緒に使う時、下記のような重要なルールがあります。

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

解決方法: 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のようなストリームを生成します。 img02.png

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

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

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

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

参考文献