なぜReact Ariaコンポーネントをサーバーコンポーネントにおいて利用するとNext.jsでビルドエラーになるのか
React Ariaのコンポーネントをサーバーコンポーネントで利用すると、Next.jsではビルドエラーとなる。
具体的には、
import { Link } from "react-aria-components";
export function FooLink() {
return <Link href="/foo">foo</Link>;
}
このようなコンポーネントがあるだけで以下のメッセージとともにビルドエラーとなる。
'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.
エラーメッセージの通り、React Ariaのコンポーネントがサーバーコンポーネントではimport
できないことによって発生するもので、React Ariaのコンポーネント種別問わず全てのコンポーネントで同様に起きるエラー。
React Ariaのコンポーネントを利用する箇所でuse client
ディレクティブを指定するなどしてクライアントコンポーネントとして扱われるようにすればビルドエラーを解消できるけど、その背景についての確認してみる。
client-onlyとは何か
まず、'client-only' cannot be imported...
で始まる上述のエラーメッセージのclient-only
とは何か。Next.jsのリポジトリーではpackages/next/src/compiled/client-only
にあるパッケージのことを指しているはず。
パッケージ内は以下のファイルだけで構成されていて、
client-only/
error.js
index.js
package.json
package.json
を見るとConditional exportsで環境に応じてimport
されるファイルを変えるようになっている。
"exports": {
".": {
"react-server": "./error.js", // 中身はthrow new Error(...)のみ
"default": "./index.js" // 中身は空っぽ
}
webpackにおいてはresolve.conditionNames
によってカスタムのConditional exportsがサポート可能のようで、Next.jsの場合を例にとるとreact-server
のConditional exportsがその形で設定されているように見える。
従ってreact-server
のConditional exportsをサポートしている状況であれば、サーバーコンポーネントにおいてはerror.js
がimport
され、それ以外の環境ではindex.js
がimport
される。
index.js
が読み込まれるとその時点で例外が投げられることになる。
なお、Next.jsのソースコードの以下の箇所から該当のエラーメッセージを見つけることができる。
ここまでみてきた内容から、client-only
は、サーバーコンポーネントでimport
したらビルドエラーにすることでモジュールの利用箇所をクライアントコンポーネントのみに制限するユーティリティーパッケージであることが分かる。
use client
ディレクティブではないのはなぜか
エラーメッセージの内容自体は理解できるとしても、ビルドエラーが発生することは少し過剰な印象も感じる。
Reactの公式ドキュメントのサードパーティライブラリーの使用に関するセクションには以下の通り記載があり、サードパーティーのライブラリー側でuse client
ディレクティブを含んでいれば、アプリケーション側ではビルド時にエラーとならず利用可能なことを期待したくなる。
ライブラリがサーバコンポーネントと互換性を有するように更新済みであれば、中に既に
'use client'
マーカが含まれていますので、サーバコンポーネントから直接使用することができます。https://ja.react.dev/reference/rsc/use-client#using-third-party-libraries
この点について、React Ariaの対応については以下のPull Requestからその背景が窺える。
React Ariaも当初はuse client
ディレクティブをつける対応を行なっていて、それをRevertしてclient-only
を入れる対応へと路線変更したのがこのPull Request。
サーバーコンポーネントからクライアントコンポーネントへ受け渡すPropsには制限があり、イベントハンドラーのようなシリアライズできない値は受け渡せない。
そのため、イベントハンドラーを扱うことの多いReact Ariaのコンポーネントを利用する場合、React Ariaの内部でuse client
ディレクティブを付けようが、結局アプリケーション側でクライアントコンポーネントを用意する必要が生じるということが変更の理由として挙げられている。
However in reality this did not work well because all of the props sent from a server component to a client component must be serializable. Therefore, things like event handlers could not be sent. Most of our components require some kind of event, so you'd end up needing to move the component that used RSP/RAC to your own client component anyway.
https://github.com/adobe/react-spectrum/pull/5826#issue-2121756169
上述のReactの公式ドキュメントのサードパーティライブラリーの使用に関するセクションにおいても、client-only
についてではないけど、同様の記述がある。
ライブラリが更新されていない場合、あるいはコンポーネントが受け取る props にクライアントでのみ指定できるイベントハンドラのようなものが含まれている場合、サードパーティのクライアントコンポーネントとそれを使用したいサーバコンポーネントの間に、自分でクライアントコンポーネントファイルを追加する必要があるかもしれません。
https://ja.react.dev/reference/rsc/use-client#using-third-party-libraries
また、以下のissueではReact Ariaのコンポーネントがサーバーコンポーネントになり得なくてもSSRは可能である[1]ことや個々のコンポーネントに応じたビルドのカスタマイズは現状難しい[2]ことにも言及がありそう。
全く別のライブラリーの話になるけどClerkは、use client
ディレクティブはアプリケーション側の関心事であるとして、React Ariaと同様にclient-only
を入れる対応を行なっている。
なお、React Ariaの類似ライブラリーの対応について知りたくなり@ark-ui/react
ではどうなのかをみてみると、ファイル名からuse client
ディレクティブの付与をハンドリングする関数がビルド時に呼ばれているように見える。
まとめ
ここまでをまとめると、なぜReact Ariaコンポーネントをサーバーコンポーネントにおいて利用するとNext.jsでビルドエラーになるのか、という疑問に関しては、
- 大抵イベントハンドラーを受け渡すなどして、結局React Ariaを利用するアプリケーション側でクライアントコンポーネントにする必要が生じるから
- 一部の例外のコンポーネント(例えば
@adobe/react-spectrum
のFlex
)のために現状のビルドを見直すのはコストバランスが見合わないから
といったあたりが現時点での答えになりそう。議論の余地があることはissueのコメント等から窺えるものの、現時点ではReact Ariaの対応方針がそうなっている。
use client
ディレクティブを入れるかどうかは、今後もライブラリーによって対応方針が分かれるところなのかもしれない。
参考
- server-only package is empty?! | Nico's Blog
- rfcs/text/0227-server-module-conventions.md at c15bc9df5afa8fd1dca6e5fd1c2ed073f7a9bd79 · reactjs/rfcs
- Server Component と Client Component で依存モジュールを切り替える | by Yosuke Kurami | Medium
- package.jsonのexportsフィールドについて
Discussion