prerenderToNodeStream

prerender は、Node.jsストリームを使用して、Reactツリーを静的HTML文字列にレンダリングします。

const {prelude} = await prerenderToNodeStream(reactNode, options?)

このAPIはNode.jsに固有です。Denoや最新のedgeランタイムなど、Web Streamsを使用する環境では、代わりにprerenderを使用してください。


リファレンス

prerenderToNodeStream(reactNode, options?)

prerenderToNodeStream を呼び出して、アプリを静的HTMLにレンダリングします。

import { prerenderToNodeStream } from 'react-dom/static';

// The route handler syntax depends on your backend framework
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js'],
});

response.setHeader('Content-Type', 'text/plain');
prelude.pipe(response);
});

クライアント側では、hydrateRootを呼び出して、サーバーで生成されたHTMLをインタラクティブにします。

以下の例を参照してください。

パラメータ

  • reactNode: HTMLにレンダリングするReactノード。たとえば、<App />のようなJSXノード。ドキュメント全体を表すことが期待されるため、Appコンポーネントは<html>タグをレンダリングする必要があります。

  • **オプション** options: 静的生成オプションを持つオブジェクト。

    • **オプション** bootstrapScriptContent: 指定された場合、この文字列はインラインの<script>タグに配置されます。
    • **オプション** bootstrapScripts: ページに出力する<script>タグの文字列URLの配列。hydrateRootを呼び出す<script>を含めるために使用します。クライアント側でReactをまったく実行したくない場合は、これを省略します。
    • **オプション** bootstrapModules: bootstrapScriptsと同様ですが、<script type="module">を出力します。
    • **オプション** identifierPrefix: ReactがuseIdによって生成されたIDに使用する文字列プレフィックス。同じページで複数のルートを使用する場合の競合を回避するのに役立ちます。hydrateRootに渡されるのと同じプレフィックスである必要があります。
    • オプション namespaceURI: ストリームのルート名前空間URIを含む文字列です。デフォルトは通常のHTMLです。SVGの場合は'http://www.w3.org/2000/svg'、MathMLの場合は'http://www.w3.org/1998/Math/MathML'を渡します。
    • オプション onError: 回復可能回復不可能かに関わらず、サーバーエラーが発生するたびに実行されるコールバックです。デフォルトでは、console.errorを呼び出すだけです。クラッシュレポートをログに記録するためにオーバーライドする場合は、console.errorも呼び出すようにしてください。また、シェルが出力される前にステータスコードを調整するためにも使用できます。
    • オプション progressiveChunkSize: チャンクのバイト数です。デフォルトのヒューリスティックの詳細はこちらをご覧ください。
    • オプション signal: アボートシグナルです。サーバーレンダリングを中止し、残りをクライアントでレンダリングすることができます。

戻り値

prerenderToNodeStream はPromiseを返します。

  • レンダリングが成功した場合、Promiseは次のプロパティを含むオブジェクトに解決されます。
    • prelude: HTMLのNode.jsストリームです。このストリームを使用して、チャンク単位でレスポンスを送信したり、ストリーム全体を文字列に読み込んだりできます。
  • レンダリングが失敗した場合、Promiseは拒否されます。フォールバックシェルを出力するために使用します。

prerenderToNodeStreamはいつ使うべきですか?

静的`prerenderToNodeStream` APIは、静的サーバーサイドジェネレーション(SSG)に使用されます。`renderToString`とは異なり、`prerenderToNodeStream`はすべてのデータがロードされるまで待ってから解決します。これにより、Suspenseを使用してフェッチする必要があるデータを含む、ページ全体の静的HTMLを生成するのに適しています。コンテンツがロードされるにつれてストリーミングするには、renderToReadableStreamのようなストリーミングサーバーサイドレンダリング(SSR)APIを使用します。


使用方法 (略:SVGアイコン)

Reactツリーを静的HTMLのストリームにレンダリングする (略:SVGアイコン)

`prerenderToNodeStream`を呼び出して、Reactツリーを静的HTMLにレンダリングし、Node.jsストリームに渡します。

import { prerenderToNodeStream } from 'react-dom/static';

// The route handler syntax depends on your backend framework
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js'],
});

response.setHeader('Content-Type', 'text/plain');
prelude.pipe(response);
});

ルートコンポーネントとともに、ブートストラップ<script>のパスリストを提供する必要があります。ルートコンポーネントは、ルート<html>タグを含むドキュメント全体を返す必要があります。

例えば、次のようになります。

export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}

Reactは、ドキュメントタイプブートストラップ<script>タグを結果のHTMLストリームに挿入します。

<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>

クライアント側では、ブートストラップスクリプトは、`hydrateRoot`を呼び出してドキュメント全体をハイドレートする必要があります。

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App />);

これにより、静的にサーバー生成されたHTMLにイベントリスナーがアタッチされ、インタラクティブになります。

詳細

ビルド出力からCSSとJSのアセットパスを読み取る (略:SVGアイコン)

JavaScriptやCSSファイルなどの最終的なアセットURLは、ビルド後にハッシュ化されることがよくあります。たとえば、styles.css の代わりに、styles.123456.css のようになります。静的アセットのファイル名をハッシュ化すると、同じアセットの異なるビルドごとに異なるファイル名が保証されます。これは、静的アセットの長期キャッシュを安全に有効にできるため便利です。特定の名前のファイルの内容が変更されることはありません。

ただし、ビルド後にアセットURLがわからない場合、ソースコードにそれらを配置する方法はありません。たとえば、前述のようにJSXに "/styles.css" をハードコーディングすることはできません。ソースコードからそれらを排除するために、ルートコンポーネントは、propとして渡されたマップから実際のファイル名を読み取ることができます。

export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}

サーバー上で、<App assetMap={assetMap} /> をレンダリングし、アセットURLを含む assetMap を渡します。

// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: [assetMap['/main.js']]
});

response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});

サーバーが <App assetMap={assetMap} /> をレンダリングするようになったため、ハイドレーションエラーを回避するために、クライアントでも assetMap を使用してレンダリングする必要があります。次のように、assetMap をシリアル化してクライアントに渡すことができます。

// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});

response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});

上記の例では、bootstrapScriptContent オプションは、クライアントでグローバルな window.assetMap 変数を設定する追加のインライン <script> タグを追加します。これにより、クライアントコードは同じ assetMap を読み取ることができます。

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App assetMap={window.assetMap} />);

クライアントとサーバーの両方が同じ assetMap propを使用して App をレンダリングするため、ハイドレーションエラーはありません。


Reactツリーを静的なHTML文字列にレンダリングする

prerenderToNodeStream を呼び出して、アプリを静的HTML文字列にレンダリングします。

import { prerenderToNodeStream } from 'react-dom/static';

async function renderToString() {
const {prelude} = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js']
});

return new Promise((resolve, reject) => {
let data = '';
prelude.on('data', chunk => {
data += chunk;
});
prelude.on('end', () => resolve(data));
prelude.on('error', reject);
});
}

これは、Reactコンポーネントの初期の非インタラクティブなHTML出力を生成します。クライアントでは、hydrateRoot を呼び出して、サーバーで生成されたHTMLを *ハイドレート* し、インタラクティブにする必要があります。


すべてのデータがロードされるのを待つ

prerenderToNodeStream は、すべてのデータのロードが完了するまで、静的HTMLの生成と解決を完了するまで待機します。たとえば、カバー、友達と写真のあるサイドバー、投稿のリストを表示するプロフィールページを考えてみましょう。

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

<Posts /> がデータをロードする必要があるとします。これには時間がかかります。理想的には、投稿が完了するまで待って、HTMLに含めたいと思うでしょう。これを行うには、Suspenseを使用してデータのサスペンドを使用できます。 prerenderToNodeStream は、サスペンドされたコンテンツが完了するまで待ってから、静的HTMLに解決します。

Suspense対応のデータソースのみがSuspenseコンポーネントをアクティブにします。 これらには以下が含まれます。

  • RelayNext.js などのSuspense対応フレームワークを使用したデータフェッチ
  • lazy を使用した遅延読み込みコンポーネントコード
  • use を使用したPromiseの値の読み取り

Suspenseは、Effectまたはイベントハンドラ内でデータがフェッチされたことを検出 しません

上記の Posts コンポーネントでデータをロードする方法は、フレームワークによって異なります。Suspense対応のフレームワークを使用している場合は、データフェッチのドキュメントに詳細が記載されています。

現状では、独自のフレームワークを使用しないSuspense対応のデータフェッチはサポートされていません。 Suspense対応のデータソースを実装するための要件は不安定であり、文書化されていません。データソースをSuspenseと統合するための公式APIは、Reactの将来のバージョンでリリースされる予定です。


トラブルシューティング

アプリ全体がレンダリングされるまでストリームが開始されない

prerenderToNodeStreamレスポンスは、すべてのサスペンス境界の解決を待って、アプリ全体のレンダリングが完了するまで待機します。これは、事前に静的サイト生成(SSG)を行うために設計されており、ロード中にさらにコンテンツをストリーミングすることはサポートしていません。

ロード中にコンテンツをストリーミングするには、renderToPipeableStreamのようなストリーミングサーバーレンダリングAPIを使用してください。