renderToReadableStream は、ReactツリーをReadable Web Streamにレンダリングします。
const stream = await renderToReadableStream(reactNode, options?)リファレンス
renderToReadableStream(reactNode, options?)
renderToReadableStream を呼び出して、ReactツリーをHTMLとして Readable 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' },
});
}クライアント側では、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'を渡します。 - オプション
nonce:nonce文字列。script-srcコンテンツセキュリティポリシー のスクリプトを許可します。 - オプション
onError: サーバーエラーが発生した際に呼び出されるコールバック関数です。 復旧可能なエラーとそうでないエラーの両方で呼び出されます。デフォルトでは、console.errorのみを呼び出します。クラッシュレポートをログに記録するようオーバーライドする場合は、console.errorを呼び出し続けるようにしてください。ステータスコードを調整するためにも使用できます。 - オプション
progressiveChunkSize: チャンクのバイト数。デフォルトのヒューリスティックの詳細については、こちらをご覧ください。 - オプション
signal: AbortSignal。これにより、サーバーサイドレンダリングの中断と、クライアント側での残りのレンダリングを行うことができます。
- オプション
戻り値
renderToReadableStream はPromiseを返します。
- シェルのレンダリングが成功した場合、そのPromiseはReadable Web Streamを解決します。
- シェルのレンダリングに失敗した場合、Promiseは拒否されます。フォールバックシェルを出力するために使用します。
返されるストリームには、追加のプロパティがあります。
allReady: シェルと追加のコンテンツの両方のレンダリングが完了したときに解決されるPromiseです。クローラーと静的生成のためにレスポンスを返す前に、await stream.allReadyを使用できます。そうすることで、プログレッシブローディングは行われなくなります。ストリームには最終的なHTMLが含まれます。
使用方法
ReactツリーをHTMLとしてReadable Web Streamにレンダリングする
renderToReadableStream を呼び出して、ReactツリーをHTMLとしてReadable 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を呼び出して、ドキュメント全体をハイドレートする必要があります。
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"をハードコーディングすることはできません。ソースコードからそれらを除外するために、ルートコンポーネントはプロップとして渡されたマップから実際のファイル名を読み取ることができます。
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オプションにより、グローバルwindow.assetMap変数をクライアント側に設定する追加のインライン<script>タグが追加されます。これにより、クライアントコードは同じassetMapを読み取ることができます。
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);クライアント側とサーバー側両方で、同じ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と、読み込み中のフォールバックをその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はさらに早くページのストリーミングを開始できます。ProfileLayoutとProfileCoverは、いずれの<Suspense>境界にもラップされていないため、最初にレンダリングを完了する必要があります。しかし、Sidebar、Friends、またはPhotosでデータを読み込む必要がある場合、Reactは代わりにBigSpinnerフォールバックのHTMLを送信します。その後、データが利用可能になるにつれて、すべてのコンテンツが表示されるまで、より多くのコンテンツが継続的に表示されます。
ストリーミングは、React自体がブラウザに読み込まれるのを待つ必要も、アプリがインタラクティブになるのを待つ必要もありません。サーバーからのHTMLコンテンツは、<script>タグが読み込まれる前に、徐々に表示されます。
シェルに含まれるものの指定
任意の<Suspense>境界の外にあるアプリの部分は、シェルと呼ばれます。
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 を持っていません。最後の手段としてサーバーサイドレンダリングに依存しないフォールバック HTML を送信するために、renderToReadableStream 呼び出しを try...catch でラップします。
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 はエラーレポートに使用し、catch ブロックはフォールバック HTML ドキュメントの送信に使用します。フォールバック 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出力を生成し、段階的に表示するのではなく、一度に表示することをお勧めします。
stream.allReady Promise を 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としてフラッシュし、残りのレンダリングをクライアント側で試みます。