renderToPipeableStream は、Reactツリーをパイプ可能なNode.js ストリームにレンダリングします。

const { pipe, abort } = renderToPipeableStream(reactNode, options?)

注記

このAPIはNode.js固有です。Denoや最新のEdgeランタイムのようなWeb Streamsを搭載した環境では、代わりにrenderToReadableStreamを使用する必要があります。


リファレンス

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);
}
});

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

下記に詳細な例を示します。

パラメーター

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

  • オプション options: ストリーミングオプションを含むオブジェクト。

    • オプション bootstrapScriptContent: 指定された場合、この文字列はインライン<script>タグに配置されます。
    • オプション bootstrapScripts: ページに出力する<script>タグの文字列URLの配列です。<script> を呼び出す hydrateRoot を含めるために使用します。クライアント側でReactを実行しない場合は、省略します。
    • オプション bootstrapModules: bootstrapScripts と同様ですが、<script type="module"> を出力します。
    • オプション identifierPrefix: 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 コンテンツセキュリティポリシー のスクリプトを許可するために使用します。
    • オプション onAllReady: シェルと追加のコンテンツの両方のレンダリングが完了したときに呼び出されるコールバックです。シェル とすべての追加の コンテンツ の両方が含まれます。クローラーや静的生成には onShellReady の代わりに使用できます。クローラーと静的生成の場合。ここでストリーミングを開始すると、段階的な読み込みは行われません。ストリームには最終的なHTMLが含まれます。
    • オプション onError: サーバーエラーが発生したときに呼び出されるコールバックです。回復可能 な場合と不可能 な場合の両方です。デフォルトでは、console.error を呼び出すだけです。クラッシュレポートを記録 するようにオーバーライドする場合は、console.error を呼び出すようにしてください。ステータスコードを調整 するためにも使用できます(シェルが出力される前)。
    • オプション onShellReady: 最初のシェル のレンダリング直後に呼び出されるコールバックです。ステータスコードを設定 し、ストリーミングを開始するためにここで pipe を呼び出すことができます。Reactは追加のコンテンツ をシェルの後に、HTML読み込みフォールバックをコンテンツに置き換えるインライン<script> タグとともにストリームします。
    • オプション onShellError: 最初のシェルのレンダリング中にエラーが発生した場合に呼び出されるコールバックです。引数としてエラーを受け取ります。ストリームからはまだバイトが出力されておらず、onShellReadyonAllReady のどちらも呼び出されません。そのため、フォールバックHTMLシェルを出力 することができます。
    • オプション progressiveChunkSize: チャンクのバイト数です。デフォルトのヒューリスティックの詳細については、こちらをご覧ください。

戻り値

renderToPipeableStream は、2つのメソッドを持つオブジェクトを返します。

  • pipe は、指定されたWritable Node.js ストリーム にHTMLを出力します。ストリーミングを有効にする場合はonShellReady で、クローラーや静的生成の場合はonAllReadypipe を呼び出します。
  • abort を使用すると、サーバー側のレンダリングを中断 し、残りをクライアント側でレンダリングできます。

使用方法

Node.js ストリームへの React ツリーの HTML レンダリング

renderToPipeableStream を呼び出して、React ツリーを Node.js ストリーム にHTMLとしてレンダリングします。

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 を呼び出してドキュメント全体をハイドレートする必要があります:

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

hydrateRoot(document, <App />);

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

詳細

ビルド出力からのCSSとJSアセットパスの読み取り

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

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

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オプションにより、グローバル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に置き換えるインライン<script>タグと共に残りのHTMLを送信します。ユーザーの視点からは、ページは最初にPostsGlimmerで表示され、後でPostsに置き換えられます。

<Suspense>境界をネストして、より詳細な読み込みシーケンスを作成することもできます。

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

この例では、Reactはさらに早くページのストリーミングを開始できます。ProfileLayoutProfileCoverは、<Suspense>境界でラップされていないため、最初にレンダリングを完了する必要があります。ただし、SidebarFriends、またはPhotosがデータを読み込む必要がある場合、Reactは代わりにBigSpinnerフォールバックのHTMLを送信します。その後、データが利用可能になるにつれて、すべてのコンテンツが表示されるまで、より多くのコンテンツが表示され続けます。

ストリーミングは、React自体がブラウザに読み込まれるのを待つ必要も、アプリケーションがインタラクティブになるのを待つ必要もありません。サーバーからのHTMLコンテンツは、<script>タグが読み込まれる前に、徐々に表示されます。

HTMLストリーミングの仕組みの詳細については、こちらをご覧ください。

注記

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

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

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

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

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


シェルに含める内容の指定

任意の<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>境界を配置して、シェルが最小限でありながら完全なもの、つまりページレイアウト全体のスケルトンのように感じるようにします。

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実装を提供する場合は、上記のようにコンソールにもエラーを出力することを忘れないでください。


シェル内のエラーからの回復

この例では、シェルにはProfileLayoutProfileCover、および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);
}
});

シェルの生成中にエラーが発生した場合、onErroronShellErrorの両方が呼び出されます。onErrorをエラーレポートに使用し、onShellErrorを使用してフォールバックHTMLドキュメントを送信します。フォールバックHTMLはエラーページである必要はありません。代わりに、クライアントのみでアプリケーションをレンダリングする代替シェルを含めることができます。


シェル外のエラーからの回復

この例では、<Posts />コンポーネントは<Suspense>でラップされているため、シェルの一部ではありません。

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

Postsコンポーネント内またはその内部でエラーが発生した場合、Reactはそれを回復しようとします。

  1. 最も近い<Suspense>境界(PostsGlimmer)の読み込み時のフォールバックをHTMLに出力します。
  2. サーバー上でのPostsコンテンツのレンダリングは「諦めます」。
  3. クライアントに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を定義し、コンポーネントからそれをスローすることができます。その後、onErroronShellReady、および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としてフラッシュし、残りの部分をクライアント側でレンダリングしようとします。