💯

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

に公開
3

革新的な 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の明示性は、単に見慣れているかどうかでは測れません。ディレクティブがモジュールの役割をアーキテクチャレベルで「宣言」するのに対し、関数ベースのアプローチはその振る舞いをビルド時の変換に委ねます。重要なのは表面的な親しみやすさではなく、その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

takanaritakanari

ディレクティブに関する別の視点を書いてくださり、ありがとうございます。
ただし、記事の内容自体とは少し違う点で気になりましたので、コメントさせていただきます。

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

この文を読んで、私が直近で読んだ別の特定の記事(あるいは特定の発言)を意識されてその反論を書かれているのではないかと思いました。(間違っていればすみません。その場合指摘いただき、このコメントを削除します。以下の指摘も見当違いです。)

この私の想像が間違いでないのであれば、記事の最初に元の主張の引用・リンクを載せてこの記事の文脈を明示する方が親切かと思います。また、その特定の記事内容を念頭に置かれていると感じたため、この記事から読んだ読者は少々問題提起部分が分かりづらいように思えました。
また反論記事であるならば、元の主張に到達できるようにしておかなければ、それはフェアな反論ではなく、"健全な議論" にもなりません。

反論対象の元の主張を明示し、読者がどちらの意見にも触れられるようにしておいた方が、より誠実で公正な反論記事となるのではないかと思います。

見当違いであれば申し訳ないです。
そうでなければ、どうかご検討ください。

oosawyoosawy

指摘ありがとうございます。

確かに最近のいくつかの記事や発言に影響を受け、最初に書き始めたときには反論記事という形でしたが、この記事で主張したかったことが、反論という形では論点が散らばってしまい一貫した解説ができないと感じたため、このような形になりました。
最近の特定のいくつかの記事や発言に限らず、長く議論の対象になっている状況の中でも、論点となる前提の理解や共有があまり広がっていないと感じていたため、特定の主張に対する反論ではなく、背景の説明や視点の提示を意識して書きました。

また最近話題となった記事で扱われていた対象や論点とは直接的に対応するものではないため言及は避けています。新たな視点や論点の整理として読んでもらえると幸いです。

takanaritakanari

返信ありがとうございます。
その気持ちもとてもわかります。わかるだけに悩ましいです。

ただ私は特に以下の部分が引っかかりました。

フレームワークロックインを生み出すどころか

これは「"use client"がフレームワークロックインを生み出す」という主張がすでにあるということをあまりにも前提にされており、おそらく反論記事として書き始めた残り香のようなものかと思います。

フレームワークロックインを生み出すのではないかという懸念[^1][^2]も見られるが・・・
・・・
[^1]: その主張をする発言 URL
[^2]: その主張をする元記事 URL

という感じに脚注で参照をしたり、あるいは記事ではなくこのコメント欄で補足するのも一つのアイデアかもしれません。

いずれにせよ修正するにはちょっと面倒な指摘でした。コメント返していただき感謝です。