renderToReadableStream

renderToReadableStream me-render sebuah React tree menjadi Readable Web Stream.

const stream = await renderToReadableStream(reactNode, options?)

Note

API ini bergantung ke Web Streams. untuk Node.js, gunakan renderToPipeableStream sebagai gantinya.


Referensi

renderToReadableStream(reactNode, options?)

Panggil renderToReadableStream untuk me-render React tree Anda ke dalam Node.js 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' },
});
}

Di klien, panggil hydrateRoot untuk membuat HTML yang dibuat server interaktif.

Lihat lebih banyak contoh di bawah ini.

Parameter

  • reactNode: Node React yang ingin Anda render ke HTML. Contohnya, sebuah elemen JSX seperti <App />. Diharapkan untuk mewakili keseluruhan dokumen, sehingga komponen App harus me-render tag <html>.

  • opsional options: Objek dengan opsi streaming.

    • opsional bootstrapScriptContent: Jika ditentukan, string ini akan ditempatkan dalam tag sebaris <script>.
    • opsional bootstrapScripts: Senarai URL string untuk tag <script> yang akan dikeluarkan di halaman. Gunakan ini untuk menyertakan <script> yang memanggil hydrateRoot. Abaikan jika Anda sama sekali tidak ingin menjalankan React pada klien.
    • opsional bootstrapModules: Seperti bootstrapScripts, tetapi mengeluarkan [<script type="module">](https://developer.mozilla.org/en-US/docs/Web/JavaScript/ Panduan/Modul) sebagai gantinya.
    • opsional identifierPrefix: Prefiks string yang digunakan React untuk ID yang dihasilkan oleh useId. Berguna untuk menghindari konflik saat menggunakan beberapa root pada halaman yang sama. Harus memiliki awalan yang sama dengan yang diteruskan ke hydrateRoot.
    • opsional namespaceURI: String dengan root namespace URI untuk streaming. Default ke HTML biasa. Tambahkan 'http://www.w3.org/2000/svg' untuk SVG atau 'http://www.w3.org/1998/Math/MathML' untuk MathML.
    • opsional nonce: String nonce digunakan untuk mengizinkan skrip untuk script-src Content-Security-Policy.
    • opsional onError: Callback yang aktif setiap kali ada kesalahan server, baik dapat dipulihkan atau tidak. Secara bawaan, ini hanya memanggil console.error. Jika Anda menimpanya ke laporan kerusakan log, pastikan Anda masih memanggil console.error. Anda juga dapat menggunakannya untuk menyesuaikan kode status sebelum shell dikeluarkan.
    • opsional progressiveChunkSize: Jumlah byte dalam potongan. Baca selengkapnya tentang heuristik default.
    • opsional signal: Sebuah abort signal yang memungkinkan Anda membatalkan render di server dan me-render sisanya pada klien.

Kembalian

renderToReadableStream mengembalikan sebuah Promise:

The returned stream has an additional property:

  • allReady: A Promise that resolves when all rendering is complete, including both the shell and all additional content. You can await stream.allReady before returning a response for crawlers and static generation. If you do that, you won’t get any progressive loading. The stream will contain the final HTML.

Stream yang dikembalikan memiliki properti tambahan:

  • allReady: Promise yang diselesaikan saat semua preose render selesai, termasuk shell dan semua konten tambahan. Anda dapat menggunakanawait stream.allReady sebelum mengembalikan respons untuk crawler dan static generation. Jika Anda melakukannya, Anda tidak akan mendapatkan pemuatan progresif. Stream akan berisi HTML final.

Penggunaan

Me-render React tree sebagai HTML ke Readable Web Stream

Panggil renderToReadableStream untuk me-render React tree anda sebagai HTML ke 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' },
});
}

Bersamaan dengan root component, Anda perlu memberikan daftar bootstrap <script> paths. Komponen root Anda harus mengembalikan seluruh dokumen termasuk tag root <html>.

Misalnya, mungkin terlihat seperti ini:

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 akan memasukkan doctype dan bootstrap <script> tags Anda ke stream HTML yang dihasilkan:

<!DOCTYPE html>
<html>
<!-- ... HTML untuk komponen Anda ... -->
</html>
<script src="/main.js" async=""></script>

Di klien, skrip bootstrap Anda harus menghidrasi seluruh dokumen dengan panggilan ke hydrateRoot:

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

hydrateRoot(document, <App />);

Ini akan melampirkan event listeners ke HTML yang dihasilkan server dan membuatnya interaktif.

Deep Dive

Membaca jalur aset CSS dan JS dari output build

The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of styles.css you might end up with styles.123456.css. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content.

However, if you don’t know the asset URLs until after the build, there’s no way for you to put them in the source code. For example, hardcoding "/styles.css" into JSX like earlier wouldn’t work. To keep them out of your source code, your root component can read the real filenames from a map passed as a prop:

URL aset final (seperti file JavaScript dan CSS) sering kali di-hash setelah dibuat. Misalnya, alih-alih styles.css Anda mungkin berakhir dengan styles.123456.css. Hashing nama file aset statis menjamin bahwa setiap build berbeda dari aset yang sama akan memiliki nama file yang berbeda pula. Ini berguna karena memungkinkan Anda mengaktifkan caching jangka panjang dengan aman untuk aset statis: konten file dengan nama tertentu tidak akan pernah berubah.

Namun, jika Anda tidak mengetahui URL aset hingga setelah pembuatan, tidak ada cara bagi Anda untuk memasukkannya ke dalam kode sumber. Misalnya, hardcoding "/styles.css" ke dalam JSX seperti sebelumnya tidak akan berfungsi. Untuk menjauhkannya dari kode sumber Anda, komponen root Anda dapat membaca nama file asli dari map yang diteruskan sebagai prop:

export default function App({ assetMap }) {
return (
<html>
<head>
<title>Aplikasiku</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}

Di server, render <App assetMap={assetMap} /> dan teruskan assetMap Anda dengan URL aset:

// Anda perlu mendapatkan JSON ini dari tooling build Anda, mis. membacanya dari keluaran build.
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' },
});
}

Karena server Anda sekarang me-render <App assetMap={assetMap} />, Anda juga perlu me-render-nya dengan assetMap pada klien untuk menghindari error hidrasi. Anda dapat men-serialize dan meneruskan assetMap ke klien seperti ini:

// Anda perlu mendapatkan JSON ini dari tooling build Anda
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Hati-hati: Aman untuk stringify() ini karena data ini tidak dibuat oleh pengguna.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

Pada contoh di atas, opsi bootstrapScriptContent menambahkan tag <script> sebaris tambahan yang menyetel variabel global window.assetMap pada klien. Ini memungkinkan kode klien membaca assetMap yang sama:

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

hydrateRoot(document, <App assetMap={window.assetMap} />);

Klien dan server merender App dengan prop assetMap yang sama, sehingga tidak ada error hidrasi.


Streaming lebih banyak konten saat dimuat

Streaming memungkinkan pengguna untuk mulai melihat konten bahkan sebelum semua data dimuat di server. Misalnya, pertimbangkan halaman profil yang menampilkan sebuah sampul, sidebar dengan teman dan foto, dan daftar postingan:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}

Bayangkan bahwa memuat data untuk <Posts /> membutuhkan waktu. Idealnya, Anda ingin menampilkan konten halaman profil lainnya kepada pengguna tanpa menunggu kiriman. Untuk melakukannya, bungkus Posts dalam batas <Suspense>:

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

Ini memberitahu React untuk memulai streaming HTML sebelum Posts memuat datanya. React akan mengirimkan HTML untuk fallback pemuatan (PostsGlimmer) terlebih dahulu, dan kemudian, ketika Posts selesai memuat datanya, React akan mengirimkan HTML yang tersisa bersama dengan tag <script> sebaris yang menggantikan fallback pemuatan dengan HTML itu. Dari perspektif pengguna, halaman pertama akan muncul dengan PostsGlimmer, kemudian diganti dengan Posts.

Anda dapat lebih jauh menyatukan batas <Suspense> untuk membuat urutan pemuatan yang lebih terperinci:

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

In this example, React can start streaming the page even earlier. Only ProfileLayout and ProfileCover must finish rendering first because they are not wrapped in any <Suspense> boundary. However, if Sidebar, Friends, or Photos need to load some data, React will send the HTML for the BigSpinner fallback instead. Then, as more data becomes available, more content will continue to be revealed until all of it becomes visible.

Streaming does not need to wait for React itself to load in the browser, or for your app to become interactive. The HTML content from the server will get progressively revealed before any of the <script> tags load.

Read more about how streaming HTML works.

Note

Only Suspense-enabled data sources will activate the Suspense component. They include:

  • Data fetching with Suspense-enabled frameworks like Relay and Next.js
  • Lazy-loading component code with lazy

Suspense does not detect when data is fetched inside an Effect or event handler.

The exact way you would load data in the Posts component above depends on your framework. If you use a Suspense-enabled framework, you’ll find the details in its data fetching documentation.

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.


Specifying what goes into the shell

The part of your app outside of any <Suspense> boundaries is called the shell:

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

It determines the earliest loading state that the user may see:

<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>

If you wrap the whole app into a <Suspense> boundary at the root, the shell will only contain that spinner. However, that’s not a pleasant user experience because seeing a big spinner on the screen can feel slower and more annoying than waiting a bit more and seeing the real layout. This is why usually you’ll want to place the <Suspense> boundaries so that the shell feels minimal but complete—like a skeleton of the entire page layout.

The async call to renderToReadableStream will resolve to a stream as soon as the entire shell has been rendered. Usually, you’ll start streaming then by creating and returning a response with that stream:

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

By the time the stream is returned, components in nested <Suspense> boundaries might still be loading data.


Logging crashes on the server

By default, all errors on the server are logged to console. You can override this behavior to log crash reports:

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

If you provide a custom onError implementation, don’t forget to also log errors to the console like above.


Recovering from errors inside the shell

In this example, the shell contains ProfileLayout, ProfileCover, and PostsGlimmer:

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

If an error occurs while rendering those components, React won’t have any meaningful HTML to send to the client. Wrap your renderToReadableStream call in a try...catch to send a fallback HTML that doesn’t rely on server rendering as the last resort:

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

If there is an error while generating the shell, both onError and your catch block will fire. Use onError for error reporting and use the catch block to send the fallback HTML document. Your fallback HTML does not have to be an error page. Instead, you may include an alternative shell that renders your app on the client only.


Recovering from errors outside the shell

In this example, the <Posts /> component is wrapped in <Suspense> so it is not a part of the shell:

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

If an error happens in the Posts component or somewhere inside it, React will try to recover from it:

  1. It will emit the loading fallback for the closest <Suspense> boundary (PostsGlimmer) into the HTML.
  2. It will “give up” on trying to render the Posts content on the server anymore.
  3. When the JavaScript code loads on the client, React will retry rendering Posts on the client.

If retrying rendering Posts on the client also fails, React will throw the error on the client. As with all the errors thrown during rendering, the closest parent error boundary determines how to present the error to the user. In practice, this means that the user will see a loading indicator until it is certain that the error is not recoverable.

If retrying rendering Posts on the client succeeds, the loading fallback from the server will be replaced with the client rendering output. The user will not know that there was a server error. However, the server onError callback and the client onRecoverableError callbacks will fire so that you can get notified about the error.


Setting the status code

Streaming introduces a tradeoff. You want to start streaming the page as early as possible so that the user can see the content sooner. However, once you start streaming, you can no longer set the response status code.

By dividing your app into the shell (above all <Suspense> boundaries) and the rest of the content, you’ve already solved a part of this problem. If the shell errors, your catch block will run which lets you set the error status code. Otherwise, you know that the app may recover on the client, so you can send “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' },
});
}
}

If a component outside the shell (i.e. inside a <Suspense> boundary) throws an error, React will not stop rendering. This means that the onError callback will fire, but your code will continue running without getting into the catch block. This is because React will try to recover from that error on the client, as described above.

However, if you’d like, you can use the fact that something has errored to set the status code:

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

This will only catch errors outside the shell that happened while generating the initial shell content, so it’s not exhaustive. If knowing whether an error occurred for some content is critical, you can move it up into the shell.


Handling different errors in different ways

You can create your own Error subclasses and use the instanceof operator to check which error is thrown. For example, you can define a custom NotFoundError and throw it from your component. Then you can save the error in onError and do something different before returning the response depending on the error type:

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

Keep in mind that once you emit the shell and start streaming, you can’t change the status code.


Waiting for all content to load for crawlers and static generation

Streaming offers a better user experience because the user can see the content as it becomes available.

However, when a crawler visits your page, or if you’re generating the pages at the build time, you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively.

You can wait for all the content to load by awaiting the stream.allReady Promise:

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

A regular visitor will get a stream of progressively loaded content. A crawler will receive the final HTML output after all the data loads. However, this also means that the crawler will have to wait for all data, some of which might be slow to load or error. Depending on your app, you could choose to send the shell to the crawlers too.


Aborting server rendering

You can force the server rendering to “give up” after a timeout:

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 will flush the remaining loading fallbacks as HTML, and will attempt to render the rest on the client.