💯

なぜ "use client" ディレクティブは優れた API なのか

に公開

革新的な API は、時としてその真価を理解されるまでに時間を要します。React Server Components そして "use client" ディレクティブもその一例でしょう。Next.js App Router とともに登場して以来、絶えず議論の的となってきたこの API について改めて、光を当ててみたいと思います。

フレームワークロックインを解消する「標準 API」

RSC における "use client" ディレクティブ API は、フレームワークロックインを生み出すどころか、むしろ解消するための革新的なアプローチです。

RSC は Next.js App Router とともに登場したために Next.js の機能の一部であるという誤解や、そのような漠然とした印象が依然として拭えないのも事実ですが、RSC の一部である "use client" を含めてすべて React 公式の API であり、これは公式ドキュメントに 'use client' directive – React が存在することからも明らかです。

そして "use client" が React 公式 API であることに大きな意味があります。

React エコシステムで統一された「標準 API」

今までの React には、サーバー側で動作するコードを扱う標準的な方法が存在していませんでした。しかし当然のことながら動的なデータフェッチを行う必要がある SSR や SSG には、サーバー側ロジックが必須で、Next.js などの React フレームワークは getServerSidePropsgetStaticProps など、各々が独自の方法でサーバー側コードを扱う API を提供してきました。それは React フレームワークが生まれた理由ともいえる根本的な機能ではあるのですが、強いていうならばこれは「フレームワーク固有の非標準 API」にほかならず、これらの"必須 API"を使うこと自体がフレームワークロックインに繋がっているとも言える状況でした。

そして React フレームワークの選択肢が増えた今こそ、フレームワーク間で共通の「標準 API」が必要とされているはずです。そしてそれに答えられる React チームが出した答えこそがサーバー側コードを扱うための「標準 API」、RSC というアーキテクチャであり、"use client" というサーバーとクライアントの境界を示すディレクティブなのです。

React に限定されない普遍的な基盤になりうるポテンシャル

さらに言うと、確かに "use client" は React チームによって発明され、React の Server Components アーキテクチャの一部として導入されましたが、その本質は全く React 固有のものではなく、他のフレームワークやツールチェインにも適用可能な普遍的なポテンシャルを持つアプローチです。

現代の、リクエスト時またはビルド時にサーバー側でコードを実行するフロントエンドフレームワークにおいて、今までの getServerSideProps を初めとした様々な手法は、すべてサーバーモジュールとして再定義されることによって、明確なサーバーとクライアントモジュールの分離をもって整理されます。

いまだ React コミュニティの中でも十分な理解が得られていない現状では想像しずらいですが、JSX が React 以外の Solid.js や Qwik などでも採用されているのと同じように、この RSC アーキテクチャがいずれ他のフレームワークに波及する可能性も十分にあります。実際に現時点でも Next.js 以外にマイナーながら Waku や @lazarv/react-server、Parcel などが RSC をサポートしています。

言語仕様で想定された正当なディレクティブ

"use client" ディレクティブについて一部では「JavaScript 標準ではない」といった誤った理解もありますが、 "use client" などの独自ディレクティブも完全に ECMAScript 仕様で当初から規定された正当なディレクティブの形です。

ECMAScript でのディレクティブは Directive Prologues の要素として下の引用のように定義されています。

簡単にまとめると、モジュールや関数の冒頭に書かれる文字列リテラル文の連続として複数のディレクティブを記述できるのですが、注記として明確に "Implementations may define implementation specific meanings" (実装は実装固有の意味を定義できる)と記載されています。

つまり、ECMAScript 仕様自体がディレクティブの拡張を明確に許容しており、"use client" はその正当な一例であると言えます。
また複数のディレクティブを併用できることを前提としているため "use client"; "use cache" も仕様上正当です。

11.2.1 Directive Prologues and the Use Strict Directive

A Directive Prologue is the longest sequence of ExpressionStatements occurring as the initial StatementListItems or ModuleItems of a FunctionBody, a ScriptBody, or a ModuleBody and where each ExpressionStatement in the sequence consists entirely of a StringLiteral token followed by a semicolon. (...)

A Use Strict Directive is an ExpressionStatement in a Directive Prologue whose StringLiteral is either of the exact code point sequences "use strict" or 'use strict'. (...)

NOTE
(...) Implementations may define implementation specific meanings for ExpressionStatements which are not a Use Strict Directive and which occur in a Directive Prologue. (...)

ECMAScript® 2025 Language Specification : 11.2.1 Directive Prologues and the Use Strict Directive

ディレクティブという然るべきアプローチ

ディレクティブに対する懐疑的な見方には、そもそも根本的な部分としてなぜそれがディレクティブであるかについて理解が十分に得られていないところもあると思います。

結果だけ見れば "use client" ディレクティブという変わった形の API が採用されたことに対して疑問を持つ人もいるかもしれませんが、その背後には長い議論と様々な形式の検討があったうえで決定された経緯があります。

React チームが最初に RSC を発表した際には "use client" ディレクティブは存在せず、代わりにファイル名で .client.js.server.js といったサフィックスを付与する方法が提案されていました。しかしこのアプローチは、ファイル名に依存するために再配布するライブラリで扱いづらく、また(最終的な RSC 仕様から見れば)クライアントモジュールへの境界であることも明確ではありません。

こうした経緯を踏まえると、RSC におけるディレクティブの採用は、思いつきで既存の "use strict" と同じ形を模倣しただけではなく、ファイル名に依存しない普遍的な方法でサーバーモジュールとクライアントモジュールの境界を明確に示すための、実用的でかつ本質的に優れた然るべきアプローチであると言えます。

標準の "use strict" と意味論が整合し、標準化提案とも協調する

まず独自拡張には既存の標準仕様や将来的な仕様策定に対する整合性への懸念がつきものですが、 特に "use client" に関しては前述したように ECMAScript で独自ディレクティブが仕様で許容されているだけではなく、すでに標準にある "use strict" とも意味論的に整合しています。

"use client" は RSC においてサーバーモジュールからクライアントモジュールへと切り替わる境界を示すディレクティブですが、これはそのモジュールに対する解釈を宣言していることに他なりません。つまり "use strict" がスクリプトや関数の解釈を厳格モードに切り替えるのと同様に、 "use client" もモジュールの解釈をクライアントモジュールとして切り替えることを宣言しているのです。

また現在 tc39 において提案されている、モジュールを定義する構文を追加する Module ExpressionsModule Declarations proposal においても、同様に機能すると考えられます。さらには将来的にこれらの proposal が採用された場合には、既存の実装の形からほとんど変わらずに、よりシンプルで直感的な構文で同様の効果を得られ、言語の進化とも協調します。

サーバーモジュールとクライアントモジュールの明確な分離

そして "use client" ディレクティブを用いてサーバーモジュールとクライアントモジュールの境界を明確に示すことは、RSC アーキテクチャにおいて重要な意味を持ちます。

多くのフレームワークで使われてきた export const getServerSidePropsexport const loader のような既存の手法は、同じソースファイルに対してクライアントコードとサーバーコードを切り出すアプローチですが、これはサーバーとクライアントのコードが同じモジュール内に混在しているため、コードの分離が曖昧です。
この手法では、minifier の使用されていないコードを削除する DCE (Dead Code Elimination) という最適化機能に頼ってクライアントバンドルとサーバーバンドルを作成するため、サーバーロジックを扱う上で本質的には安全とはいえません。

サーバー側ロジックとクライアント側実装が同じモジュールで定義されるため、それぞれでのみ使用するつもりで書いた import であっても互いに参照したり、影響を与えることが可能です。そのため不適切な参照や、最適化の挙動によっては、クライアントバンドルにサーバーコードが含まれたり、最悪の場合シークレットが漏洩したりするリスクがあります。

対して RSC ではクライアントモジュールとサーバーモジュールをモジュールレベルで明確に分離し、クライアントモジュールには "use client" ディレクティブを用いて明示的に宣言することで境界を明確に定義します。
当然ながら変わらずモジュール境界でのデータの受け渡しに注意は必要ですが、1つのモジュールに2つの環境のコードが混在することによるリスクは解消されます。

見せかけの API に「魔法」を隠蔽せず、明示的に宣言するアプローチ

RSC におけるディレクティブとは対照的なアプローチとして、TanStack Start などでは createServerFn() のような一般的な関数を用いた API が採用されています。
このような関数 API は非常に一般的であり、なじみ深いために一見すると自然に思えますが、実際には「魔法」が隠蔽されていることに注意が必要です。

まずクライアントとサーバーを透過的に接続するという点で、React 標準のアプローチも、TanStack Start のアプローチも、ビルド時にバンドラによって多くの作業が行われることには変わりありません。しかし両者のアプローチには大きな違いがあります。

React のディレクティブなどを用いるアプローチでは、モジュールの解釈を宣言し、モジュール内の export などに対して規約を与えるため、ディレクティブを認識する以外には特別な「魔法」を使わず直接的にモジュールを認識できます。
またディレクティブを認識しモジュールの解釈を切り替える動作についても、バンドラが ECMAScript ホスト環境であると言えるため、自然な動作だと理解できます。

一方で createServerFn() のような関数ベースのアプローチは異なります。このアプローチではモジュールに規約を課す代わりに、関数呼び出し自体が規約です。これが意味するところは、何の変哲もない関数呼び出しの裏には、ビルド時に構文上から特定の関数呼び出しのパターンを検出し、構文的な変換を行うコンパイラマジックが潜んでいるということです。

この違いは、API の明示性と透明性に大きな影響を与えます。ディレクティブを用いるアプローチでは、開発者はモジュールの解釈を明示的に宣言し、そのモジュールがどのように扱われるかを理解できます。一方で関数ベースのアプローチでは、関数呼び出しの背後にあるコンパイラマジックが隠蔽されているため、開発者はその挙動を完全に理解することが難しくなります。

まとめ

React Server Components が App Router とともに登場して以降の React コミュニティでは、React のあり方について頻繁に議論が巻き起り、二分化しているような状況が続いています。しかし React の API がまだシンプルに見えていたころから、その背後には根拠づけられた強い理論と設計思想があり、それが React を React たらしめながら今に至るまで成長し続けてきた原動力であると思います。そしてそれは今になっても全く変わらず、React チームや Meta での基礎研究や実践的な試行錯誤の積み重ねが React の絶え間ない進化を支えています。

React Hooks の登場以降、内部的なアーキテクチャの刷新として Fiber, Fizz, Flight などの開発で目新しい機能追加があまりない期間が続きましたが、その準備期間を終え React は再び新しい時代へと急速に進化を遂げています。

初めて JSX を目にしたとき、誰もが抵抗を感じたと思います。自分もそうでした。しかしそれは十分すぎるほど実用的で、今では JSX は React 開発のゆるぎない前提として受け入れられ、React エコシステムを中心に広く採用されています。

また React の API には、いつも深い洞察と考え抜かれた設計思想が反映されており、 "use client" ディレクティブもまさにその一例です。React Server Components そして "use client" ディレクティブこそが、React エコシステムの「標準 API」として、フレームワークのベースラインを押し上げ、エコシステムが発展していく鍵になると信じています。

新しい技術に対する活発で健全な議論は、コミュニティの発展に不可欠です。しかし、その議論が API の表面的な形に終始するのではなく、その背後にある設計思想や、それが解決しようとしている課題、そして目指す未来像に向けられるとき、私たちはより建設的な一歩を踏み出せるのではないでしょうか。 "use client" は単なる文字列ではありません。それは、React が Web 開発の複雑さに立ち向かい、次のステージへと進化するための、静かですが、確かな意志表示なのです。

Discussion