React Server Components

'use client'React Server Components 用の機能です。

'use client' を使い、どのコードがクライアントで実行されるかをマークします。


リファレンス

'use client'

ファイルのトップに 'use client' を加えることで、当該モジュールとそれが連動してインポートしている依存モジュールがクライアントコードであるとマークします。

'use client';

import { useState } from 'react';
import { formatDate } from './formatters';
import Button from './button';

export default function RichTextEditor({ timestamp, text }) {
const date = formatDate(timestamp);
// ...
const editButton = <Button />;
// ...
}

'use client' でマークされているファイルがサーバコンポーネントからインポートされた場合、互換性のあるバンドラは当該モジュールのインポートを、サーバで実行されるコードとクライアントで実行されるコードの境界として扱います。

上記では formatDateButtonRichTextEditor が依存するモジュールですので、これらのモジュール自体に 'use client' ディレクティブが含まれているかどうかに関わらず、これらもクライアントで評価されます。ある単一のモジュールが、サーバコードからインポートされた場合はサーバで、クライアントコードからインポートされた場合はクライアントで評価される場合があることに注意してください。

注意点

  • 'use client' はファイルの冒頭、すなわちインポートや他のコードより先になければなりません(コメントは OK です)。シングルクォートまたはダブルクォートで書かれていなければならず、バックティックは無効です。
  • 'use client' モジュールが別のクライアントレンダーされるモジュールからインポートされた場合、ディレクティブの効果はありません。
  • コンポーネントモジュールに 'use client' ディレクティブが含まれている場合、そのコンポーネントは必ずクライアントコンポーネントであることが保証されます。しかしコンポーネントに直接 'use client' ディレクティブがなくとも、クライアントで評価されることがあります。
    • コンポーネントがクライアントコンポーネントと見なされるのは、それが 'use client' ディレクティブを含むモジュールで定義されている場合、またはそれが 'use client' ディレクティブを含むモジュールの間接的な依存モジュールである場合です。それ以外の場合、サーバコンポーネントとなります。
  • クライアントで評価されるようマークされるコードとはコンポーネントに限りません。クライアントモジュールのサブツリーに含まれるすべてのコードは、クライアントに送信され、クライアントで実行されます。
  • サーバで評価されるモジュールが 'use client' のモジュールから値をインポートする場合、その値は React コンポーネントであるか、またはクライアントコンポーネントに渡せるようサポート済のシリアライズ可能な props の型のいずれかでなければなりません。それ以外の方法で使用すると例外がスローされます。

'use client' がクライアントコードをマークする方法

React アプリでは、コンポーネントはしばしば別々のファイル、すなわちモジュールに分割されます。

React Server Components を使用するアプリでは、デフォルトでアプリはサーバでレンダーされます。'use client'モジュール依存関係ツリーにサーバ・クライアント境界を導入、つまり実質的にはクライアントモジュールのサブツリーの作成を行います。

これをより具体的に示すために、以下の React Server Components アプリを考えてみましょう。

import FancyText from './FancyText';
import InspirationGenerator from './InspirationGenerator';
import Copyright from './Copyright';

export default function App() {
  return (
    <>
      <FancyText title text="Get Inspired App" />
      <InspirationGenerator>
        <Copyright year={2004} />
      </InspirationGenerator>
    </>
  );
}

このサンプルアプリのモジュール依存関係ツリーでは、InspirationGenerator.js に書かれた 'use client' ディレクティブが、当該モジュールとそのすべての間接的な依存モジュールをクライアントモジュールとしてマークします。これで InspirationGenerator.js から始まるサブツリー全体がクライアントモジュールとなるのです。

トップノードがモジュール 'App.js' を表す木構造のグラフ。'App.js'には 'Copyright.js'、'FancyText.js'、'InspirationGenerator.js' の 3 つの子ノードがある。'InspirationGenerator.js'には 'FancyText.js'と'inspirations.js' の 2 つの子ノードがある。'InspirationGenerator.js'を含む下のノードには黄色い背景色が付けられており、'InspirationGenerator.js'の 'use client' ディレクティブによってこのサブグラフがクライアント側でレンダーされることを示している。
トップノードがモジュール 'App.js' を表す木構造のグラフ。'App.js'には 'Copyright.js'、'FancyText.js'、'InspirationGenerator.js' の 3 つの子ノードがある。'InspirationGenerator.js'には 'FancyText.js'と'inspirations.js' の 2 つの子ノードがある。'InspirationGenerator.js'を含む下のノードには黄色い背景色が付けられており、'InspirationGenerator.js'の 'use client' ディレクティブによってこのサブグラフがクライアント側でレンダーされることを示している。

'use client' は React Server Components アプリのモジュール依存関係ツリーを分割し、InspirationGenerator.js とそのすべての依存モジュールをクライアントレンダーされるものとしてマークする

レンダー中、フレームワークはルートコンポーネントから始め、レンダーツリーを順にレンダーしていきますが、その際にクライアントとマークされたものからインポートされたコードの評価を選択的に除外します。

その後レンダーツリーのうちサーバでレンダーされた部分が、クライアントに送信されます。クライアントは、ダウンロードしたクライアントコードを用いて、ツリーの残りの部分のレンダーを完了します。

各ノードがコンポーネントを表し、その子要素を子コンポーネントとして表すツリーグラフ。トップレベルのノードは 'App' とラベル付けされ、2つの子コンポーネント 'InspirationGenerator' と 'FancyText' を持っています。'InspirationGenerator' は2つの子コンポーネント、'FancyText' と 'Copyright' を持っています。'InspirationGenerator' とその子コンポーネント 'FancyText' はクライアントレンダリングされるとマークされています。
各ノードがコンポーネントを表し、その子要素を子コンポーネントとして表すツリーグラフ。トップレベルのノードは 'App' とラベル付けされ、2つの子コンポーネント 'InspirationGenerator' と 'FancyText' を持っています。'InspirationGenerator' は2つの子コンポーネント、'FancyText' と 'Copyright' を持っています。'InspirationGenerator' とその子コンポーネント 'FancyText' はクライアントレンダリングされるとマークされています。

React Server Components アプリのレンダーツリー。InspirationGenerator とその子コンポーネント FancyText は、クライアントとマークされたコードからエクスポートされたコンポーネントであるため、クライアントコンポーネントと見なされる

ここで以下の定義を導入しましょう。

  • クライアントコンポーネントとは、レンダーツリーの中の、クライアントでレンダーされるコンポーネントです。
  • サーバコンポーネントとは、レンダーツリーの中の、サーバでレンダーされるコンポーネントです。

上記のサンプルアプリでは、AppFancyTextCopyright はすべてサーバでレンダーされるのでサーバコンポーネントと見なされます。InspirationGenerator.js とその間接的な依存モジュールはクライアントコードとしてマークされているため、コンポーネント InspirationGenerator とその子コンポーネントである FancyText はクライアントコンポーネントとなります。

さらに深く知る

FancyText がサーバコンポーネントでありクライアントコンポーネントでもある理由

上記の定義によれば、コンポーネント FancyText は、サーバコンポーネントでもありクライアントコンポーネントでもあることになります。どういうことでしょうか?

まずは “コンポーネント” という用語があまり厳密ではないことを明示的に意識しましょう。“コンポーネント” とは以下の 2 つの意味で使用されます。

  1. 「コンポーネント」はコンポーネントの定義を指すことがあります。ほとんどの場合、これは関数です。
// This is a definition of a component
function MyComponent() {
return <p>My Component</p>
}
  1. 「コンポーネント」はまた、その定義に従って使用されている個々のコンポーネントを指すこともあります。
import MyComponent from './MyComponent';

function App() {
// This is a usage of a component
return <MyComponent />;
}

ここが厳密でなくとも大抵の場合は概念を説明する際に問題とはなりませんが、この場合はそうではありません。

サーバコンポーネントやクライアントコンポーネントについて話すとき、コンポーネントの個々の使用法を指しています。

  • コンポーネントが 'use client' ディレクティブを含んだモジュールで定義されている場合や、コンポーネントが他のクライアントコンポーネントからインポートされコールされる場合、そのコンポーネントの使用法はクライアントコンポーネントであるということになります。
  • それ以外の場合、そのコンポーネントの使用法はサーバコンポーネントだということになります。
各ノードがコンポーネントを表し、その子供を子コンポーネントとして表すツリーグラフ。トップレベルのノードは 'App' とラベル付けされており、'InspirationGenerator' と 'FancyText' の 2 つの子コンポーネントを持っている。'InspirationGenerator' は 'FancyText' と 'Copyright' の 2 つの子コンポーネントを持っている。'InspirationGenerator' とその子コンポーネント 'FancyText' はクライアントでレンダリングされるとマークされている。
各ノードがコンポーネントを表し、その子供を子コンポーネントとして表すツリーグラフ。トップレベルのノードは 'App' とラベル付けされており、'InspirationGenerator' と 'FancyText' の 2 つの子コンポーネントを持っている。'InspirationGenerator' は 'FancyText' と 'Copyright' の 2 つの子コンポーネントを持っている。'InspirationGenerator' とその子コンポーネント 'FancyText' はクライアントでレンダリングされるとマークされている。
コンポーネントの「使用法」を表すのがレンダーツリー

FancyText の問題に戻りましょう。分かるのは、コンポーネントの定義として 'use client' ディレクティブがないこと、そして使用法として 2 種類あることです。

FancyTextApp の子として使用されている場合、その使用法はサーバコンポーネントです。FancyTextInspirationGenerator の下でインポートされて呼び出されている場合、InspirationGenerator'use client' ディレクティブが含まれているため、その FancyText の使用法はクライアントコンポーネントとなります。

つまり、FancyText のコンポーネント定義がサーバ上で評価される一方で、クライアントコンポーネントとして使用されるためクライアントにダウンロードもされる、ということになります。

さらに深く知る

Copyright はクライアントコンポーネントである InspirationGenerator の子としてレンダーされているので、それがサーバコンポーネントになっていることに驚くかもしれません。

'use client' がサーバとクライアントのコードの境界を、レンダーツリーではなくモジュール依存関係ツリー上で定義することを思い出してください。

トップノードがモジュール 'App.js' を表すツリーグラフ。'App.js' には 'Copyright.js'、'FancyText.js'、'InspirationGenerator.js' の 3 つの子ノードがある。'InspirationGenerator.js' には 'FancyText.js' と 'inspirations.js' の 2 つの子ノードがある。'InspirationGenerator.js' 自体とそれ以下のノードは背景が黄色になっており、'InspirationGenerator.js' に 'use client' ディレクティブがあるためこのサブグラフがクライアントでレンダーされることを示している。
トップノードがモジュール 'App.js' を表すツリーグラフ。'App.js' には 'Copyright.js'、'FancyText.js'、'InspirationGenerator.js' の 3 つの子ノードがある。'InspirationGenerator.js' には 'FancyText.js' と 'inspirations.js' の 2 つの子ノードがある。'InspirationGenerator.js' 自体とそれ以下のノードは背景が黄色になっており、'InspirationGenerator.js' に 'use client' ディレクティブがあるためこのサブグラフがクライアントでレンダーされることを示している。

'use client' はモジュール依存ツリー上でサーバとクライアントのコードの境界を定義する

モジュール依存関係ツリー上では、App.jsCopyright.js モジュールから Copyright をインポートして呼び出していることがわかります。Copyright.js には 'use client' ディレクティブが含まれていないため、このコンポーネントはサーバレンダーで使用されています。App はルートコンポーネントでありサーバ上でレンダーされるからです。

クライアントコンポーネントがサーバコンポーネントをレンダーできるのは、JSX を props として渡すことができるためです。上記の例の場合、InspirationGeneratorchildren として Copyright を受け取ります。しかし、InspirationGenerator モジュールは Copyright モジュールを直接インポートしたり、コンポーネントを呼び出したりしているわけではありません。それらはすべて App によって行われます。実際、Copyright コンポーネントは InspirationGenerator のレンダーすら始まらないうちに完全に実行されます。

つまり、コンポーネント間に親子関係があるからといって、同じ環境でレンダーされることが保証されるわけではない、ということを覚えておきましょう。

'use client' をいつ使うべきか

'use client' を使うことで、どのコンポーネントがクライアントコンポーネントであるかを決定できます。デフォルトはサーバコンポーネントですので、その利点と制限事項を簡単に概観することで、何をクライアントレンダーとしてマークする必要があるかを判断できるようにしましょう。

話を簡単にするためサーバコンポーネントについて説明しますが、サーバで実行されるアプリのすべてのコードに同じ原則が適用されます。

サーバコンポーネントの利点

  • サーバコンポーネントにより、クライアントに送信され実行されるコードの量を減らすことができます。バンドルされてクライアントで評価されるのはクライアントモジュールだけです。
  • サーバコンポーネントにはサーバ上で実行されることに伴う利点があります。ローカルファイルシステムにアクセスでき、データフェッチやネットワークリクエストのレイテンシが低い可能性があります。

サーバコンポーネントの制限事項

  • サーバコンポーネントはユーザによるインタラクションをサポートできません。イベントハンドラはクライアントで登録されトリガされる必要があるためです。
    • 例えば、onClick のようなイベントハンドラはクライアントコンポーネントでのみ定義できます。
  • サーバコンポーネントではほとんどのフックを使用できません。
    • サーバコンポーネントがレンダーされるとき、その出力は本質的にクライアントでレンダーすべきコンポーネントのリストになります。サーバコンポーネントはレンダー後にメモリに残らないため、state を持つことはできません。

サーバコンポーネントから返せるシリアライズ可能な型

React アプリケーションでは一般的に、親コンポーネントから子コンポーネントにデータを渡します。これらが異なる環境でレンダーされるため、サーバコンポーネントからクライアントコンポーネントにデータを渡す際には特別な配慮が必要です。

サーバコンポーネントからクライアントコンポーネントに渡される props の値は、シリアライズ可能 (serializable) である必要があります。

シリアライズ可能な props には以下のものがあります:

以下のものはサポートされていません。

  • クライアントとマークされたモジュールからエクスポートされていない、または 'use server' でマークされていない関数
  • クラス
  • 任意のクラスのインスタンス(上記の組み込みクラスを除く)や、null プロトタイプのオブジェクト
  • グローバルに登録されていないシンボル、例:Symbol('my new symbol')

使用法

ユーザ操作や state を実装する

'use client';

import { useState } from 'react';

export default function Counter({initialValue = 0}) {
  const [countValue, setCountValue] = useState(initialValue);
  const increment = () => setCountValue(countValue + 1);
  const decrement = () => setCountValue(countValue - 1);
  return (
    <>
      <h2>Count Value: {countValue}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </>
  );
}

Counter は値を増減させるために useState フックとイベントハンドラを必要とするので、このコンポーネントはクライアントコンポーネントでなければならず、ファイル冒頭に 'use client' ディレクティブが必要です。

対照的に、インタラクションなしで UI をレンダーするコンポーネントはクライアントコンポーネントである必要はありません。

import { readFile } from 'node:fs/promises';
import Counter from './Counter';

export default async function CounterContainer() {
const initialValue = await readFile('/path/to/counter_value');
return <Counter initialValue={initialValue} />
}

例えば、Counter の親コンポーネントである CounterContainer は、インタラクティブではなく state を使用しないため、'use client' は必要ありません。むしろ CounterContainer はサーバコンポーネントでなければなりません。サーバ上のローカルファイルシステムから読み取っており、それが可能なのはサーバコンポーネントだけだからです。

また、サーバやクライアント固有の機能を使用しないため、レンダーされるべき場所に関して関知しないコンポーネントもあります。先ほどの例でいうと、FancyText がそのようなコンポーネントです。

export default function FancyText({title, text}) {
return title
? <h1 className='fancy title'>{text}</h1>
: <h3 className='fancy cursive'>{text}</h3>
}

このような場合、'use client' ディレクティブを追加しません。その結果、サーバコンポーネントから参照されたときに、FancyText の(ソースコードではなく)出力結果が、ブラウザに送信されます。先ほどのアプリの例で示したように、FancyText はどこからインポートされ、どこで使用されるかによって、サーバコンポーネントになることもクライアントコンポーネントになることもあるのです。

しかし、FancyText の HTML 出力が(依存モジュールも含んだ)ソースコードに比べて大きいような場合は、必ずクライアントコンポーネントとなるように強制する方が効率的かもしれません。一例として、長い SVG パス文字列を返すようなコンポーネントは、強制的にクライアントコンポーネントとしてレンダーする方が効率的である可能性があるでしょう。

クライアント API の使用

あなたの React アプリでは、Web ストレージ、オーディオやビデオの操作、デバイスハードウェア、その他もろもろのブラウザ API といった、クライアント固有の API を使用することがあるでしょう。

以下の例では、コンポーネントは DOM API を使用して canvas 要素を操作しています。これらの API はブラウザでのみ利用可能なので、クライアントコンポーネントとしてマークする必要があります。

'use client';

import {useRef, useEffect} from 'react';

export default function Circle() {
const ref = useRef(null);
useLayoutEffect(() => {
const canvas = ref.current;
const context = canvas.getContext('2d');
context.reset();
context.beginPath();
context.arc(100, 75, 50, 0, 2 * Math.PI);
context.stroke();
});
return <canvas ref={ref} />;
}

サードパーティライブラリの使用

React アプリでは、一般的な UI パターンやロジックを処理するためにサードパーティのライブラリがよく用いられます。

これらのライブラリがコンポーネントのフックやクライアント API に依存することがあります。以下のような React API を使用するサードパーティのコンポーネントは、クライアント上で実行する必要があります。

ライブラリがサーバコンポーネントと互換性を有するように更新済みであれば、中に既に 'use client' マーカが含まれていますので、サーバコンポーネントから直接使用することができます。ライブラリが更新されていない場合、あるいはコンポーネントが受け取る props にクライアントでのみ指定できるイベントハンドラのようなものが含まれている場合、サードパーティのクライアントコンポーネントとそれを使用したいサーバコンポーネントの間に、自分でクライアントコンポーネントファイルを追加する必要があるかもしれません。