renderToPipeableStream
renderToPipeableStream
は React ツリーをパイプ可能な Node.js ストリームにレンダーします。
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
リファレンス
renderToPipeableStream(reactNode, options?)
renderToPipeableStream
を呼び出して、React ツリーを HTML として Node.js ストリームにレンダーします。
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
クライアント側では、このようにサーバ生成された 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-src
Content-Security-Policy を用いてスクリプトを許可するためのnonce
文字列。 - 省略可能
onAllReady
: シェルとすべての追加コンテンツの両方を含むすべてのレンダーが完了したときに呼び出されるコールバック。クローラや静的生成向けの場合に、onShellReady
の代わりに利用できます。ここからストリーミングを開始する場合、プログレッシブなローディングがなくなり、ストリームには最終的な HTML が含まれるようになります。 - 省略可能
onError
: サーバエラーが発生するたびに発火するコールバック。復帰可能なエラーの場合もそうでないエラーの場合もあります。デフォルトではconsole.error
のみを呼び出します。これを上書きしてクラッシュレポートをログに記録する場合でもconsole.error
を呼び出すようにしてください。また、シェルが出力される前にステータスコードを調整するためにも使用できます。 - 省略可能
onShellReady
: 初期シェルがレンダーされた直後に呼び出されるコールバック。ここでステータスコードのセットを行い、pipe
をコールしてストリーミングを開始できます。React はシェルを送信した後に、追加コンテンツと、ローディングフォールバックをそれで置換するためのインライン<script>
タグをストリーミングします。 - 省略可能
onShellError
: 初期シェルのレンダー中にエラーが発生すると呼び出されるコールバック。引数としてエラーを受け取ります。ストリームからのバイト列送信はまだ起きておらず、onShellReady
やonAllReady
はコールされなくなるため、フォールバック用の HTML シェルを出力することができます。 - 省略可能
progressiveChunkSize
: チャンクのバイト数。デフォルトの推論方法についてはこちらを参照してください。
- 省略可能
返り値
renderToPipeableStream
は以下の 2 つのメソッドを含んだオブジェクトを返します。
pipe
: HTML を Node.js の Writable ストリームに出力します。pipe
の呼び出しは、ストリーミングを有効にしたい場合はonShellReady
で、クローラや静的生成向けの出力を行いたい場合はonAllReady
で行ってください。abort
: サーバでのレンダーを中止してクライアントで残りをレンダーするために使用します。
使用法
React ツリーを HTML として Node.js ストリームにレンダーする
renderToPipeableStream
を呼び出して、React ツリーを HTML として Node.js ストリーム にレンダーします。
import { renderToPipeableStream } from 'react-dom/server';
// The route handler syntax depends on your backend framework
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
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 は 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>
...
<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('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
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('/', (request, response) => {
const { pipe } = renderToPipeableStream(<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']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
上記の例では、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>
境界は適切に配置して、シェルがミニマルかつ完全に感じられるように必要があるでしょう。例えばページレイアウト全体のスケルトンのようなものです。
シェル全体のレンダーが完了したときに onShellReady
コールバックが呼び出されます。通常、ここでストリーミングを開始します。
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
onShellReady
が呼び出される時点では、ネストされた <Suspense>
バウンダリ内のコンポーネントはまだデータをロード中かもしれません。
サーバ上でのクラッシュログの記録
デフォルトでは、サーバ上のすべてのエラーはコンソールにログとして記録されます。この挙動をオーバーライドして、クラッシュレポートをログとして記録することができます。
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
カスタムの onError
実装を提供する場合、上記のようにエラーをコンソールにもログとして記録することを忘れないでください。
シェル内のエラーからの復帰
この例では、シェルとして ProfileLayout
、ProfileCover
、および PostsGlimmer
が含まれています。
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
これらのコンポーネントをレンダーする際にエラーが発生した場合、React はクライアントに送信できる意味のある HTML を提供できません。最終手段として、onShellError
をオーバーライドして、サーバレンダリングに依存しないフォールバック HTML を送信しましょう。
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
シェルの生成中にエラーが発生した場合、onError
と onShellError
の両方が発火します。エラーレポートには onError
を使用し、フォールバックの HTML ドキュメントを送信するためには onShellError
を使用します。フォールバック 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>
バウンダリより上の部分)とそれ以外のコンテンツにアプリを分割することで、この問題はすでに部分的に解決されています。シェルでエラーが発生した場合、onShellError
コールバックが呼び出され、エラーのステータスコードをセットすることができます。それ以外の場合は、アプリがクライアント上で復帰できる可能性があるため、“OK” を送信できるのです。
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
シェルの外側(つまり <Suspense>
バウンダリの内側)のコンポーネントでエラーが発生した場合、React はレンダーを停止しません。これは、onError
コールバックは発火するものの、その後 onShellError
ではなく onShellReady
が発火することを意味します。これは上記で説明したように、React がそのエラーをクライアント上で復帰しようとするからです。
ただしお望みであれば、何らかのエラーが起きたという事実に基づいたステータスコードを設定することもできます。
let didError = false;
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
これは、初期のシェルコンテンツの生成中に既にシェルの外側で発生したエラーが捕捉できるだけなので、完全ではありません。あるコンテンツでエラーが発生したかどうかを知ることが重要であれば、それをシェルに移動させることができます。
エラーの種類によって処理を分ける
カスタムの Error
サブクラスを作成し、instanceof
演算子を使用してどんなエラーがスローされたかをチェックすることができます。例えば、カスタムの NotFoundError
を定義し、コンポーネントからそれをスローすることができます。その後、onError
、onShellReady
, onShellError
のコールバック中でエラーの種類に応じて異なる処理を行うことができます。
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
しかしシェルを出力してストリーミングを開始してしまうと、ステータスコードを変更できなくなりますので注意してください。
クローラや静的生成向けに全コンテンツの読み込みを待機する
ストリーミングにより、利用可能になった順でコンテンツをユーザが見えるようになるため、ユーザ体験が向上します。
しかし、クローラがページを訪れた場合や、ビルド時にページを生成している場合には、コンテンツを徐々に表示するのではなく、すべてのコンテンツを最初にロードしてから最終的な HTML 出力を生成したいでしょう。
onAllReady
コールバックを用いることで、すべてのコンテンツが読み込まれるまで待機を行うことができます。
let didError = false;
let isCrawler = // ... depends on your bot detection strategy ...
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
if (!isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady() {
if (isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
通常のユーザは、ストリームで読み込まれるコンテンツを段階的に受け取ります。クローラは、全データが読み込まれた後の最終的な HTML 出力を受け取ります。しかし、これはクローラがすべてのデータを待つ必要があることを意味し、その中には読み込みが遅いものやエラーが発生するものも含まれるかもしれません。アプリケーションによっては、クローラにもシェルを送信することを選択しても構いません。
サーバレンダリングの中止
タイムアウト後にサーバでのレンダーを「諦める」ように強制することが可能です。
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);
React は、残りのローディング中フォールバックを HTML として直ちに出力し、クライアント上で残りをレンダーしようと試みます。