renderToReadableStream
renderToReadableStream は React ツリーを読み取り可能な Web Stream にレンダーします。
const stream = await renderToReadableStream(reactNode, options?)リファレンス
renderToReadableStream(reactNode, options?)
renderToReadableStream を呼び出して、React ツリーを HTML として読み取り可能な Web Stream にレンダーします。
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
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'を渡します。 - 省略可能
nonce:script-srcContent-Security-Policy を用いてスクリプトを許可するためのnonce文字列。 - 省略可能
onError: サーバエラーが発生するたびに発火するコールバック。復帰可能なエラーの場合もそうでないエラーの場合もあります。デフォルトではconsole.errorのみを呼び出します。これを上書きしてクラッシュレポートをログに記録する場合でもconsole.errorを呼び出すようにしてください。また、シェルが出力される前にステータスコードを調整するためにも使用できます。 - 省略可能
progressiveChunkSize: チャンクのバイト数。デフォルトの推論方法についてはこちらを参照してください。 - 省略可能
signal: サーバでのレンダーを中止してクライアントで残りをレンダーするために使用できる abort signal。
- 省略可能
返り値
renderToReadableStream は Promise を返します。
- シェルのレンダーが成功した場合、Promise は読み取り可能な Web Stream に解決 (resolve) されます。
- シェルのレンダーが失敗した場合、Promise は拒否 (reject) されます。これを使用してフォールバックシェルを出力します。
返されるストリームには以下の追加のプロパティが存在します。
allReady: シェルとすべての追加コンテンツの両方を含むすべてのレンダーが完了したときに解決される Promise。クローラや静的生成向けの場合、レスポンスを返す前にstream.allReadyを await できます。これを行うとプログレッシブなローディングがなくなり、ストリームには最終的な HTML が含まれるようになります。
使用法
React ツリーを HTML として読み取り可能な Web Stream にレンダーする
renderToReadableStream を呼び出して、React ツリーを HTML として読み取り可能な Web Stream にレンダーします。
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
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 stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(stream, {
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 stream = await renderToReadableStream(<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(stream, {
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 をレンダーするため、ハイドレーションエラーは発生しません。
ロードが進むにつれてコンテンツをストリーミングする
ストリーミングにより、サーバ上ですべてのデータがロードされる前に、ユーザがコンテンツを見始められるようにすることができます。例えば以下のようなプロフィールページがあり、カバー、フレンド・写真が含まれたサイドバー、投稿のリストを表示しているところを考えましょう。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}ここで、<Posts /> のデータを読み込むのに時間がかかるとしましょう。理想的には、投稿の読み込みを待つことなく、プロフィールページの残りのコンテンツをユーザに表示したいでしょう。これを実現するには、Posts を <Suspense> バウンダリで囲みます。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}これにより React に、Posts のデータが読み込まれる前に HTML をストリーミング開始するよう指示します。React はまず、ローディングフォールバック (PostsGlimmer) の HTML を送信します。次に Posts のデータ読み込みが完了したら、残りの HTML と、ローディングフォールバックをそれで置換するためのインライン <script> タグを送信します。ユーザから見ると、ページにはまず PostsGlimmer が表示され、後からそれが Posts に置き換わることになります。
さらに、より細かく読み込みシーケンスを制御するために<Suspense> バウンダリをネストさせることもできます。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}この例では、React はページのストリーミングをさらに素早く開始できます。最初にレンダーが完了している必要があるのは、<Suspense> バウンダリで囲まれていない ProfileLayout と ProfileCover だけです。Sidebar、Friends、Photos がデータを読み込む必要がある場合、React は BigSpinner のフォールバック HTML を代わりに送信します。その後、より多くのデータが利用可能になるにつれ、より多くのコンテンツが表示されていき、最終的にすべてが表示されます。
ストリーミングでは、ブラウザで React 自体が読み込まれるのを待つ必要も、アプリが操作可能になるのを待つ必要もありません。サーバからの HTML コンテンツは、あらゆる <script> タグが読み込まれる前にプログレッシブに表示されます。
シェルに何を含めるかの指定
アプリの全 <Suspense> バウンダリより外にある部分のことをシェル (shell) と呼びます。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}これが、ユーザに見える最初のローディング中状態を決定します。
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>もしルート部分でアプリ全体を <Suspense> バウンダリでラップしてしまうと、シェルとしてはそのスピナだけが含まれることになります。しかしこれはあまり快適なユーザ体験にはなりません。大きなスピナが画面に表示されることは、もう少しだけ待ってから実際のレイアウトを表示することよりも遅く不快に感じられるためです。したがって、<Suspense> 境界は適切に配置して、シェルがミニマルかつ完全に感じられるように必要があるでしょう。例えばページレイアウト全体のスケルトンのようなものです。
非同期の renderToReadableStream 呼び出しが stream に解決されるのは、シェル全体のレンダーが終了した直後です。通常、その stream を使ってレスポンスを作成して返すことで、ストリーミングを開始します。
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}この stream が返される時点では、ネストされた <Suspense> バウンダリ内のコンポーネントはまだデータをロード中かもしれません。
サーバ上でのクラッシュログの記録
デフォルトでは、サーバ上のすべてのエラーはコンソールにログとして記録されます。この挙動をオーバーライドして、クラッシュレポートをログとして記録することができます。
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}カスタムの onError 実装を提供する場合、上記のようにエラーをコンソールにもログとして記録することを忘れないでください。
シェル内のエラーからの復帰
この例では、シェルとして ProfileLayout、ProfileCover、および PostsGlimmer が含まれています。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}これらのコンポーネントをレンダーする際にエラーが発生した場合、React はクライアントに送信できる意味のある HTML を提供できません。最終手段として、renderToReadableStream 呼び出しを try...catch でラップして、サーバレンダリングに依存しないフォールバック HTML を送信しましょう。
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}シェルの生成中にエラーが発生した場合、onError と catch ブロックの両方が発火します。エラーレポートには onError を使用し、フォールバックの HTML ドキュメントを送信するためには catch ブロックを使用します。フォールバック HTML はエラーページである必要はありません。代わりに、クライアントのみでアプリをレンダーするための代替シェルを含めることも可能です。
シェル外のエラーからの復帰
この例では、<Posts /> コンポーネントは <Suspense> でラップされているため、シェルの一部ではありません。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}Posts コンポーネントまたはその内部のどこかでエラーが発生した場合、React はそこからの復帰を試みます。
- 最も近い
<Suspense>バウンダリ (PostsGlimmer) のローディングフォールバックを HTML として出力します。 - サーバ上で
Postsのコンテンツをレンダーしようとするのを諦めます。 - JavaScript コードがクライアント上でロードされると、React はクライアント上で
Postsのレンダーを再試行します。
クライアント上で Posts のレンダーを再試行して再度失敗した場合、React はクライアント上でエラーをスローします。レンダー中にスローされる他のすべてのエラーと同様に、最も近い親のエラーバウンダリがユーザにエラーをどのように提示するかを決定します。つまり、エラーが復帰不能であることが確定するまで、ユーザにはローディングインジケータが見えることになります。
クライアント上での Posts のレンダー再試行が成功した場合、サーバからのローディングフォールバックはクライアントでのレンダー出力で置き換えられます。ユーザにはサーバエラーが発生したことは分かりません。ただし、サーバの onError コールバックとクライアントの onRecoverableError コールバックが発火するため、エラーについて通知を受け取ることができます。
ステータスコードの設定
ストリーミングにはトレードオフも存在します。ユーザがコンテンツを早く見ることができるように、できるだけ早くページのストリーミングを開始したいでしょう。一方で、ストリーミングを開始すると、レスポンスのステータスコードを設定することができなくなります。
シェル(すべての <Suspense> バウンダリより上の部分)とそれ以外のコンテンツにアプリを分割することで、この問題はすでに部分的に解決されています。シェルでエラーが発生した場合、catch ブロックが実行され、エラーのステータスコードをセットすることができます。それ以外の場合は、アプリがクライアント上で復帰できる可能性があるため、“OK” を送信できるのです。
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}シェルの外側(つまり <Suspense> バウンダリの内側)のコンポーネントでエラーが発生した場合、React はレンダーを停止しません。これは、onError コールバックが発火するものの、コードは catch ブロックに入らずに実行を続けることを意味します。これは上記で説明したように、React がそのエラーをクライアント上で復帰しようとするからです。
ただしお望みであれば、何らかのエラーが起きたという事実に基づいたステータスコードを設定することもできます。
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}これは、初期のシェルコンテンツの生成中に既にシェルの外側で発生したエラーが捕捉できるだけなので、完全ではありません。あるコンテンツでエラーが発生したかどうかを知ることが重要であれば、それをシェルに移動させることができます。
エラーの種類によって処理を分ける
カスタムの Error サブクラスを作成し、instanceof 演算子を使用してどんなエラーがスローされたかをチェックすることができます。例えば、カスタムの NotFoundError を定義し、コンポーネントからそれをスローすることができます。その後、onError でエラーを保存しておき、エラーの種類に応じてレスポンスを返す前に何か異なる処理を行うことができます。
async function handler(request) {
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}しかしシェルを出力してストリーミングを開始してしまうと、ステータスコードを変更できなくなりますので注意してください。
クローラや静的生成向けに全コンテンツの読み込みを待機する
ストリーミングにより、利用可能になった順でコンテンツをユーザが見えるようになるため、ユーザ体験が向上します。
しかし、クローラがページを訪れた場合や、ビルド時にページを生成している場合には、コンテンツを徐々に表示するのではなく、すべてのコンテンツを最初にロードしてから最終的な HTML 出力を生成したいでしょう。
Promise である stream.allReady を await することで、すべてのコンテンツが読み込まれるまで待機を行うことができます。
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}通常のユーザは、ストリームで読み込まれるコンテンツを段階的に受け取ります。クローラは、全データが読み込まれた後の最終的な HTML 出力を受け取ります。しかし、これはクローラがすべてのデータを待つ必要があることを意味し、その中には読み込みが遅いものやエラーが発生するものも含まれるかもしれません。アプリケーションによっては、クローラにもシェルを送信することを選択しても構いません。
サーバレンダリングの中止
タイムアウト後にサーバレンダリングを「諦める」ように強制することができます。
async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);
const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...React は、残りのローディング中フォールバックを HTML として直ちに出力し、クライアント上で残りをレンダーしようと試みます。