Server Component と Client Component で依存モジュールを切り替える

Yosuke Kurami
6 min readApr 2, 2024

ちょっとした React Server Component 小ネタ。

Next.js (webpack, Turbopack) で確認しているが、おそらく RSC に対応しているツールであったらどれも変わらないはず。

アプリケーションの package.json の imports セクションに以下のように記載しておく。util の部分は好きな文字列で構わないが # から始めておくこと。

{
"imports": {
"#util": {
"react-server": "./src/util.react-server.ts",
"default": "./src/util.default.ts"
}
}
}

React Server 環境とそれ以外の環境用、それぞれの実装を用意する。 とりあえず結果が異なることを確認したければ以下のような感じ。

/* ./src/util.default.ts */

export function awesomeFn() {
return "Default env";
}
/* ./src/util.react-server.ts */

export function awesomeFn() {
return "React Server env";
}

基本的に準備はこれで終わり。あとはアプリケーションのコンポーネントで "#util" からインポートするように書けば、そのコンポーネントが SC(Server Component) か CC(Client Component) かによって、解決されるモジュール実態が切り替わる。

import { awesomeFn } from "#util";

// このコンポーネントが SC か CC かによって出力結果が切り替わる
function MyComponent() {
return <div>{awesomeFn()}</div>;
}

仕組

冒頭に記載した package.json の imports の部分は Node.js の Subpath imports と呼ばれる機能。

Node.js 自体が気にする Condition 名は importrequire であるが、Community Conditions として任意の値を利用することができる (たとえば、 TypeScript は import, require, default に加えて、types というキーを識別する)。

React は React Server 環境を表す目的で react-server を使ようになっている。React 本体もそうであるが、server-onlyclient-only パッケージの利用例が一番わかりやすい。 server-only の package.json は以下のようになっていて、React Server 環境以外だと index.js が読み込まれ、その瞬間にエラーとなる仕掛けになっている。

{
"name": "server-only",
"files": ["index.js", "empty.js"],
"main": "index.js",
"exports": {
".": {
"react-server": "./empty.js",
"default": "./index.js"
}
}
}
/* index.js */

throw new Error(
"This module cannot be imported from a Client Component module. " +
"It should only be used from a Server Component."
);

一方で、React や 上記の server-only を使うためには、メタフレームワーク側が「これは React Server 環境で実行していますよ」を伝える必要があり、例えば Next.js は webpack の resolve.conditionNames を用いている。Turbopack の実装はちゃんと確認していないが、おそらく似たような設定項目があるのだと思う(手元で next --turbo で確認したらちゃんと動いたし)。

使い所

紹介はしてみたものの、アプリケーションサイドがカジュアルに使い倒すものでは無いと考えている。

似たような機構に React Native の Platform Specific Extensions (.ios.js とか .android.js などで実行環境ごとのモジュールを切り替えるヤツ) がある。 iOS / Android の違いは横並びの概念であるが、RSC 環境かどうかというのは横並びというよりはむしろ事前・事後の関係に近い感覚なので、実装の異なるモジュールを import したいという要件が発生する頻度は React Native と比べると低いのではないかと思う。 実際、App Router の実案件をやっていてこのテクニックが必要だった箇所というのも思い当たらない。

また、TypeScript は Subpath imports について defualttypes までは認識するが、react-server を認識するわけではない。use server な Component から import "#util" にコードジャンプしても CC 向けである default の実装を開くため、デフォルト側の実装にコメントなりを記載しておかないと、Conditional Imports を利用していることが伝わらないだろうし。

--

--

Yosuke Kurami

Front-end web developer. TypeScript, Angular and Vim, weapon of choice.