`SyntaxError: Named export 'renderToReadableStream' not found.`を解決する
要約
対象
- Node.js環境で
react-dom/server
のrenderToReadableStream
を使用する場合 - (より具体的には)React Router v7 + Cloudflareでプリレンダリングやビルドファイルを用いたスクリプトを実行する場合
解決法
ライブラリからインポートを行うとき、指定した条件キーを用いて解決するようNode.jsに命令します。
--conditions <key>
または-C <key>
のようにして指定することができます。
node --conditions workerd ./server.js
// or
NODE_OPTIONS='-C workerd' npm run your-script
Windowsユーザーの場合、上記コマンドを実行すると停止するためcross-env
を用いてnpm scriptsとして実行します。
npm i cross-env
{
"scripts": {
"dev": "cross-env NODE_OPTIONS=\"-C workerd\" npm run your-script"
}
}
ちなみにworkerd
は、react-dom@19
かつCloudflareを用いる場合の条件キーです。react-dom@18
およびBunやDenoなどを用いる場合は次の表のように読み替えてください。
ランタイム | react-dom@19 |
react-dom@18 |
---|---|---|
Cloudflare | workerd |
browser |
Bun | bun |
browser |
Deno | deno |
deno |
その他 |
browser またはedge-light など |
browser |
問題の所在
サーバー上でReact コンポーネントをHTMLにレンダーしたい場合、renderToReadableStream
を用いることがあります。
このコードをNode.js環境で実行[1]しようとすると次のようなエラーが発生します。
import { renderToReadableStream } from 'react-dom/server'
SyntaxError: Named export 'renderToReadableStream' not found. The requested module 'react-dom/server' is a CommonJS module, which may not support all module.exports as named exports.
この問題に関連するものとして以下を参照してください。
- Bug: there is no renderToReadableStream function in react-dom@18.2.0 in Node.js
- Node.js環境でも
renderToReadableStream
が使いたい!
要約すると、react-dom/server
からインポートを行うときエッジ環境とNode.js環境で読み込まれるファイルが異なります。そしてNode.js環境で実行するファイル内ではrenderToReadableStream
が定義されていないため上記エラーが発生します。
--conditions
を変更する
Node.jsの指定条件に応じて異なる実行ファイルを提供するという機能は、Node.jsのConditional Exportsに関するものです。
具体的な動作を確認してみましょう。
(react-dom@19
のpackage.json
から一部省略して引用)
{
"exports": {
"./server": {
"workerd": "./server.edge.js",
"node": "./server.node.js"
},
}
}
- Node.jsではデフォルトの条件として
"node"
,"default"
,"import"
,"require"
が常に指定され解決対象となります(ソース)。 -
react-dom/server
からのインポートを試みるとき、package.json
で指定された"node"
キーのマッピングに対応した./server.node.js
からファイルが読み込まれますが、./server.node.js
ではrenderToReadableStream
が定義されていないためエラーが発生します。
ここでNode.jsの--conditons
を用いて追加指定し、解決対象を変更すれば実行ファイルを変えることができます。
コマンドラインオプションから変更する場合は、--conditions
または-C
を用います。(ソース)
node -C workerd ./server.js
また環境変数から変更することも可能です。
NODE_OPTIONS='-C workerd' npm run your-script
Windowsユーザーの場合、上記コマンドを実行すると停止するためcross-env
を用いてnpm scriptsとして実行します。
npm i cross-env
{
"scripts": {
"dev": "cross-env NODE_OPTIONS=\"-C workerd\" npm run your-script"
}
}
なお条件は複数指定できます。react-dom
の場合細かく条件分けされているのですが、ライブラリによっては単にbrowser
とdefault
のみ提供されている可能性もあるのでworkerd
, worker
, browser
と複数指定しておくとよいでしょう。
node -C workerd -C worker -C browser" ./server.js
// or
NODE_OPTIONS='-C workerd -C worker -C browser' npm run your-script
条件の優先順位
ここで疑問が生じます。workerd
を明示的に指定するとき常にnode
に優先するのでしょうか。あるいは条件を複数指定する場合、どの値が優先して読み込まれるのでしょうか。
Within the "exports" object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. The general rule is that conditions should be from most specific to least specific in object order. - ソース
つまり、exports
オブジェクトのなかで指定されたキーの順番に応じて、読み込みファイルを決定するということです。--conditions
, -C
で明示的に指定したことや指定した順序は読み込みファイルの優先順位に関係ありません。
この点について簡単なリポジトリを用意しています。npm run dev
を実行する際に--conditions
を変更すると、ログに出力される環境も変更されます。具体的には以下の動作です。
-
-C workerd
に設定するとThis is worked environment!
と出力されます。 -
-C node
に設定するとThis is node environment!
と出力されます。 -
-C browser
に設定するとThis is node environment!
と出力されます。これはライブラリのexport
オブジェクトにおいてnode
のほうがbrowser
より優先順位が高いからです。
別の方法
インポートパスを直接指定する
さて既に参照した記事にあるようにインポートパスを直接指定することでも動作します。
import { renderToReadableStream } from 'react-dom/server.browser' // 'react-dom/server'ではない!
しかし、この方法についてバグが発生する可能性について言及したものもあります。またライブラリによってはエクスポートのキーマッピングが変更されアプリが壊れる可能性も否定できません[2]。
一方で条件を指定するというこの投稿の方法が常に万能というわけではありません。前述のようにexports
オブジェクトのなかで指定されたキーの順番に応じて、読み込みファイルを決定されます。仮にライブラリがnode
をworkerd
より上位に指定している場合、条件にworkerd
を指定したところでnode
が読み込まれてしまうということを意味します[3]。必要に応じて使い分けましょう。
Viteを用いている場合
Viteを用いた開発については、ssr.resolve.conditions
、ssr.resolve.externalConditions
を用いてください。
export default defineConfig({
ssr: {
resolve: {
conditions: [
'workerd',
'worker',
'browser',
],
externalConditions: ['workerd', 'worker']
},
},
})
React Router v7 + CloudflareでPre-renderingするとエラーが出る問題
React Router v7(投稿時点でv7.1.1) + Cloudflareにおいて(Node.js環境で)開発できるのにPre-renderingを使用するとエラーが発生する問題があります。
コードを軽く流し読みした程度で間違えている可能性もあるのですおそらく次のような動作をしています。開発自体はViteを用いて動作します。一方でPre-renderingは、Viteを用いるのではなく、Node.js上でビルドファイルを読み込みリクエストをシミュレーションすることで行われます。そこでViteの設定とは別にNode.jsの条件指定をする必要があります。
buildコマンドを次のように変更してください。
{
"scripts": {
- "build": "react-router build",
+ "build": "cross-env NODE_OPTIONS=\"--conditions workerd\" react-router build",
},
"devDependencies": {
+ "cross-env": "^7"
}
}
RemixのCloudflare workersテンプレートをReact Router v7にアップグレードしてPre-renderingが動作するように調整したこちらのリポジトリおよびコミットも参照してください。
その他参照
-
一般にNode.js環境では
renderToPipeableStream
を使うことになりますが、Web標準上にサーバーを実装したいということもあると思います。現にHonoをNode.jsで動かす向きもあります。また、より一般的な事例としてはエッジ環境向けにrenderToReadableStream
を用いて作成したファイルを、様々な処理のためにNode.js環境で読みこむことを想定しています。 ↩︎ -
実をいうと、この記事を作成した理由はインポートのpathを直接指定する方法を用いていたところ、React@18からReact@19へのアップデートで
export
オブジェクトに変更がありモジュールを読み込めなくなったという問題に起因します。 ↩︎ -
この点についてStandardize condition order so that edge-lite preferred over browser #29877を参照してください。 ↩︎
Discussion