クライアントコンポーネント
クライアントサイドとインタラクティブにやり取りしたい場合、クライアントコンポーネントを使用できます。クライアントコンポーネントもサーバー側でレンダリングされますが、クライアント側でハイドレーションされます。
クライアントコンポーネントはすべて非同期に読み込まれるため、ページのレンダリングを妨げることはありません。すべてESモジュールにコンパイルされ、必要な時だけ読み込まれます。
クライアントコンポーネントを作成するには、ファイルの冒頭に"use client";を追記してください。
"use client";
export default function MyClientComponent() {
return <p>This is a client component</p>;
}
クライアントコンポーネントはすべてのReactフックで使用可能です。例えば、useStateフックやonClickのようなイベントハンドラとも併用できます。クライアントコンポーネントはサーバー側とクライアント側の両方でレンダリングされます。
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
クライアントコンポーネントはサーバーコンポーネントからpropsを受け取ることができます。propsはシリアル化された状態でクライアント側に受け渡されます。クライアント側に受け渡す全てのpropsはシリアル化可能なものでなければいけません。propsとして基本的な値、配列、オブジェクトを渡すことができますが、関数を渡すことはできません。
"use client";
export default function MyClientComponent({ name }) {
return <p>Hello {name}</p>;
}
上記のクライアント・コンポーネントをサーバー・コンポーネントから使用する:
import MyClientComponent from "./MyClientComponent";
export default function MyServerComponent() {
return <MyClientComponent name="John" />;
}
サーバーコンポーネントをクライアントコンポーネントでラップすることもできます。React Contextプロバイダーのように、クライアントコンポーネント内でサーバーコンポーネントを使いたいときに非常に便利です。このコンテキストはすべての子コンポーネントで利用できます。コンテキストはクライアント側でのみ作成されるので、サーバーコンポーネントからはアクセスできません。
"use client";
import { createContext } from "react";
const MyContext = createContext("unknown");
export default function MyProvider({ name, children }) {
return <MyContext.Provider value={name}>{children}</MyContext.Provider>;
}
import MyProvider from "./MyProvider";
export default async function MyServerComponent() {
const name = await getUserName();
return (
<MyProvider name={name}>
<p>Hello {name}</p>
</MyProvider>
);
}
クライアントコンポーネントごとに別ファイルを作成する代わりに、関数本体の中で"use client"ディレクティブを使用できます。これにより、使用するサーバーコンポーネントのすぐ隣にクライアントコンポーネントをインラインで定義できます。
import { useState } from "react";
function Counter() {
"use client";
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default function App() {
return (
<div>
<h1>My App</h1>
<Counter />
</div>
);
}
上記のCounter関数は自動的に別のクライアントコンポーネントモジュールとして抽出されます。サーバーコンポーネントAppは関数そのものではなく、Counterへのクライアント参照をレンダリングします。
他の関数内でもインラインクライアントコンポーネントを使用できます。フレームワークは親スコープからキャプチャされた変数を自動的に検出し、抽出されたクライアントコンポーネントにpropsとして渡します。
import { useState } from "react";
export default function App() {
const label = "clicks";
const Counter = () => {
"use client";
const [count, setCount] = useState(0);
return (
<div>
<p>{label}: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
return (
<div>
<h1>My App</h1>
<Counter />
</div>
);
}
この例では、label変数がAppのスコープからキャプチャされ、クライアントコンポーネントにpropsとして自動的に転送されます。インラインコンポーネントが使用するモジュールレベルのインポートやトップレベルの宣言は、抽出されたモジュールに直接含まれます。
注意: キャプチャされた変数はすべて、通常のクライアントコンポーネントのpropsと同様にシリアル化可能でなければなりません。関数、クラスインスタンス、その他のシリアル化できない値はキャプチャできません。
アプリのルートモジュール — react-serverに渡すエントリファイル — 自体が"use client"モジュールである場合、ランタイムは起動時にこれを検出し、React DOMのrenderToReadableStreamを直接通してレンダリングします。RSCフライトパイプラインは完全にスキップされます。有効化する設定は何もなく、解決済みルートモジュールのディレクティブに基づいてレンダリングパスが自動的に選択されます。この選択はプロセスの存続期間中、アプリ全体に適用されます。
src/index.ssr.jsx"use client";
import App from "./App.jsx";
export default function Root() {
return <App />;
}
src/index.rsc.jsximport App from "./App.jsx";
export default function Root() {
return <App />;
}
上記の2つのエントリは、同じHTMLと同じハイドレーション後のReactツリーを生成します。最初のエントリはReact DOM SSRを直接通ります。2番目のエントリはRSCパイプラインを通り、App("use client"モジュール)はフライトペイロード内でクライアント参照として具現化されます。
children propは、サーバーエントリがクライアントルートの中にサーバーコンポーネントを合成するための橋渡しになります:
src/index.rsc.jsximport App from "./App.jsx"; // "use client"
import Stats from "./Stats.jsx"; // サーバーコンポーネント
import Products from "./Products.jsx"; // サーバーコンポーネント
export default function Root() {
return (
<App>
<Stats />
<Products />
</App>
);
}
このRSCエントリでは、<Stats />と<Products />はサーバーコンポーネントとして評価されます。フライトペイロードには、Appのchildren propとしてアタッチされた、事前レンダリング済みのReact要素ツリーが含まれます。これらに対してクライアントサイドJavaScriptは配信されません。
同じJSXを"use client"エントリに記述すると、importは推移的にクライアントバンドルに取り込まれ、<Stats />と<Products />はクライアントコンポーネントになります — それらはReact DOMを通してSSRされ、通常通りハイドレーションされます:
src/index.ssr.jsx"use client";
import App from "./App.jsx";
import Stats from "./Stats.jsx";
import Products from "./Products.jsx";
export default function Root() {
return (
<App>
<Stats />
<Products />
</App>
);
}
両方のエントリで同じコンポーネントファイルが使用されます。エントリのディレクティブが、それらの扱われ方を決定します。
クライアントルートレンダリングでは、"use cache: request"関数は引き続き動作します。ランタイムは解決済みキャッシュエントリをself.__react_server_request_cache_entries__ペイロードとしてHTMLストリームにフラッシュし、ブラウザ側のラッパーがハイドレーション中に同期的にそこから読み取ります。クライアントコンポーネント内のuse(cachedFn())呼び出しは、サスペンドすることなく初回で解決されます。
POSTリクエスト経由で呼び出されるサーバー関数は、ショートカットの唯一の例外です:アプリのルートがクライアントモジュールであっても、標準のRSCエントリにルーティングされます。サーバー関数のディスパッチには、serverFunctionResultフライトを発行するための完全なRSCパイプラインが必要だからです。これは作成者には見えません — サーバー関数は実行され、useActionStateは他のRSCルートと全く同じように結果を消費します。
アプリ全体がいずれにせよクライアントツリーになる場合 — インタラクティブ性が広く行き渡ったダッシュボード形式のアプリ、またはSPAシェル — ではクライアントルートレンダリングを採用してください。ほとんどがサーバーレンダリングされ、いくつかのインタラクティブなアイランドを含むアプリでは、通常のRSCパイプラインが適切なデフォルトのままです — その方が配信するJavaScriptが少なくなります。
ClientOnlyコンポーネントでコンポーネントをラップすることで、クライアント専用のコンポーネントを作成できます。ClientOnly コンポーネントの子コンポーネントは、クライアント側でのみレンダリングされます。
import { ClientOnly } from "@lazarv/react-server/client";
export default function MyServerComponent() {
return (
<div>
<p>This is rendered on the server side</p>
<ClientOnly>
<p>This is rendered on the client side</p>
<MyClientComponent />
</ClientOnly>
</div>
);
}
ClientOnlyはレンダリングガードです — 子コンポーネントがいつレンダリングされるかは制御しますが、何がバンドルされるかは制御しません。ラップされたコンポーネントとそのインポートは依然としてSSRバンドルに含まれ、サーバーレンダリング中に実行されます。ブラウザ専用の重い依存関係(WebGL、Canvas、IntersectionObserverベースのライブラリ、モジュールスコープでwindowに触れるものすべて)を読み込むコンポーネントには、以下の"use client; no-ssr"ディレクティブを使用してください — 実装そのものをSSRバンドルから完全に取り除きます。
"use client"コンポーネントは、SSR中に依然としてサーバーでレンダリングされます — クライアントに延期されるのはそのインタラクティブ性だけです。そのモジュールグラフはSSRバンドルの一部であり、取り込まれるすべてのインポートはサーバーでバンドルされ評価されます。依存関係がブラウザでしか意味を持たないコンポーネント — Three.js、チャートライブラリ、コードエディタ、モジュールスコープでwindowやdocumentに触れるもの — の場合、これは空のラッパーをレンダリングするためだけに数百KiBのコードをエッジワーカーに配信し(そして解析する)ことを意味します。
"use client; no-ssr"ディレクティブはこれを回避します。ランタイムに対して、モジュールを環境ごとに異なる方法でコンパイルするよう指示します:
- サーバービルド: モジュールはnullを返すスタブに置き換えられます。元のコードもそのインポートも、SSRバンドルには現れません。
- クライアントビルド: モジュールは
ClientOnlyで自動的にラップされます。実コンポーネントは通常通り依存関係をインポートしますが、ハイドレーション後にのみレンダリングされます — サーバーのnull出力と一致し、ハイドレーション不一致を回避します。
src/components/Scene.jsx"use client; no-ssr";
import { useEffect, useRef } from "react";
import * as THREE from "three";
export default function Scene() {
const ref = useRef(null);
useEffect(() => {
const renderer = new THREE.WebGLRenderer();
ref.current.appendChild(renderer.domElement);
// ...
return () => renderer.dispose();
}, []);
return <div ref={ref} />;
}
サーバーコンポーネントから使用すると、インポートは他のクライアントコンポーネントとまったく同じに見えます:
import Scene from "./components/Scene.jsx";
export default function Page() {
return (
<main>
<h1>Welcome</h1>
<Scene />
</main>
);
}
サーバーは<main><h1>Welcome</h1></main>をレンダリングします — <Scene />のマークアップも、Three.jsの評価も、SSRバンドル内のThree.jsのコードもありません。ページがハイドレーションされた後、ブラウザはSceneチャンクを読み込み、エフェクトを実行し、その場所にcanvasをマウントします。
次の場合に"use client; no-ssr"を使用してください:
- コンポーネントがモジュールスコープでブラウザ専用のグローバル(
window、document、navigator、WebGL/Canvasコンテキスト)に依存する。 - 依存関係グラフが大きく、ブラウザでのみ意味を持つ(3Dシーン、リッチテキストエディタ、動画プレイヤー、DOM計測を伴うチャートライブラリ)。
- そのモジュールを、コンポーネントが現れるページでのみ読み込まれる別のクライアントチャンクにしたい。
ブラウザ専用の依存関係を持たないインタラクティブなコンポーネントには、通常の"use client"が適切なデフォルトです — ハイドレーションが完了するまでにユーザーが目にするSSRマークアップを無料で得られます。
注意:
ClientOnlyと同様に、"use client; no-ssr"コンポーネントはハイドレーション後までは何もレンダリングしません。代替(SSRマークアップなし)が許容できるコンポーネントに留めて、レイアウトの安定性のために周囲にプレースホルダをレンダリングすることを検討してください。