prerender
prerender
は React ツリーを Web Stream を用いて静的な HTML 文字列にレンダーします。
const {prelude} = await prerender(reactNode, options?)
リファレンス
prerender(reactNode, options?)
prerender
を呼び出して、アプリを静的な HTML にレンダーします。
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
クライアント側では、このようにサーバ生成された HTML を操作可能にするために hydrateRoot
を用います。
引数
-
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
: サーバでのレンダーを中止してクライアントで残りをレンダーするために使用できる abort signal。
- 省略可能
返り値
prerender
は Promise を返します。
- レンダーが成功した場合、プロミスは以下を含んだオブジェクトに解決 (resolve) されます。
prelude
: HTML の Web Stream。このストリームを使ってレスポンスを送信したり、ストリームを文字列に一括して読み出したりできます。
- レンダーが失敗した場合は、Promise は拒否 (reject) されます。これを使用してフォールバックシェルを出力します。
使用法
React ツリーを静的な HTML としてストリームにレンダーする
prerender
を呼び出して、React ツリーを静的な HTML として読み取り可能な Web Stream にレンダーします。
import { prerender } from 'react-dom/static';
async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
ルートコンポーネント と ブートストラップ <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 は doctype とあなたが指定したブートストラップ <script>
タグを結果の HTML ストリームに注入します。
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>
クライアント側では、ブートストラップスクリプトは hydrateRoot
を呼び出して document
全体のハイドレーションを行う必要があります。
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
これにより、サーバで生成された静的な HTML にイベントリスナが追加され、操作可能になります。
さらに深く知る
ビルド後に、最終的なアセット URL(JavaScript や CSS ファイルなど)にはよくハッシュ化が行われます。例えば、styles.css
が styles.123456.css
になることがあります。静的なアセットのファイル名をハッシュ化することで、同じアセットがビルドごとに異なるファイル名になることが保証されます。これが有用なのは、ある特定の名前を持つファイルの内容が不変になり、静的なアセットの長期的なキャッシングを安全に行えるようになるためです。
しかし、ビルド後までアセット URL が分からない場合、それらをソースコードに含めることができません。例えば、先ほどのように JSX に "/styles.css"
をハードコーディングする方法は動作しません。ソースコードにそれらを含めないようにするため、ルートコンポーネントが、props 経由で渡されたマップから実際のファイル名を読み取るようにすることができます。
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'
};
async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
サーバで <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'
};
async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}
上記の例では、bootstrapScriptContent
オプションを使って<script>
タグを追加して、クライアント上でグローバル window.assetMap
変数をセットしています。これにより、クライアントのコードが同じ assetMap
を読み取れるようになります。
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
クライアントとサーバの両方が props として同じ assetMap
を使って App
をレンダーするため、ハイドレーションエラーは発生しません。
React ツリーを静的な HTML 文字列にレンダーする
prerender
を呼び出して、アプリを静的な HTML 文字列にレンダーします。
import { prerender } from 'react-dom/static';
async function renderToString() {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
const reader = stream.getReader();
let content = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return content;
}
content += Buffer.from(value).toString('utf8');
}
}
これにより、React コンポーネントの、操作できない初期 HTML が生成されます。クライアントでは、hydrateRoot
を呼び出してこのサーバで生成された HTML に対するハイドレーションを行い、操作可能にする必要があります。
全データの読み込みを待機
prerender
は、静的な HTML 生成を行って解決する前に、全データがロードされるのを待機します。例えば以下のようなプロフィールページがあり、カバー、フレンド・写真が含まれたサイドバー、投稿のリストを表示しているところを考えましょう。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
ここで、<Posts />
のデータを読み込むのに時間がかかるとしましょう。理想的には、投稿の読み込みを待機して、HTML に含めてしまいたいとします。これを実現するには、サスペンスを使ってそのデータをサスペンドします。prerender
はそのサスペンドされているコンテンツの読み込みを待機してから、静的 HTML へと解決します。
トラブルシューティング
アプリ全体がレンダーされるまでストリームが始まらない
prerender
の返り値は解決する前に、全サスペンスバウンダリが解決することも含む、アプリ全体のレンダーの終了を待機します。これは事前静的サイト生成 (SSG) のために設計されているものであり、コンテンツを読み込みながらのストリーミングをサポートしません。
コンテンツを読み込みながらストリームしたい場合は、サーバレンダー API である renderToReadableStream などを使用してください。