renderToPipeableStream

renderToPipeableStream me-render pohon (tree) React menjadi Node.js Stream yang pipeable.

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

Note

API ini spesifik untuk Node.js. Environment dengan Web Streams, seperti Deno dan edge runtime modern, harus menggunakan renderToReadableStream sebagai gantinya.


Referensi

renderToPipeableStream(reactNode, options?)

Panggil renderToPipeableStream untuk me-render pohon React Anda sebagai HTML menjadi Node.js Stream.

import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});

Di sisi klien, panggil hydrateRoot untuk membuat HTML yang dihasilkan server menjadi interaktif.

Lihat contoh lainnya.

Parameter

  • reactNode: Node React yang ingin anda render menjadi HTML. Contohnya, sebuah elemen JSX seperti <App />. Ini diharapkan mewakili keseluruhan dokumen. Jadi, komponen App harus me-render tag <html>.

  • options (opsional): Objek berisi opsi streaming.

    • bootstrapScriptContent (opsional): Jika ditentukan, string ini akan diletakkan di dalam tag <script> inline.
    • bootstrapScripts (opsional): Senarai URL string untuk tag <script> yang akan di-emit (dipancarkan) di halaman. Gunakan ini untuk menyertakan <script> yang memanggil hydrateRoot. Abaikan jika Anda sama sekali tidak ingin menjalankan React di sisi klien.
    • bootstrapModules (opsional): Sama seperti bootstrapScripts, tetapi meng-emit <script type="module"> sebagai gantinya.
    • identifierPrefix (opsional): Awalan string yang digunakan React untuk ID yang dihasilkan oleh useId. Berguna untuk menghindari konflik saat menggunakan banyak root di halaman yang sama. Harus sama dengan awalan yang dioper ke hydrateRoot.
    • namespaceURI (opsional): String dengan URI namespace root untuk streaming. Default ke HTML biasa. Berikan 'http://www.w3.org/2000/svg' untuk SVG atau 'http://www.w3.org/1998/Math/MathML' untuk MathML.
    • nonce (opsional): String nonce untuk mengizinkan skrip untuk script-src Content-Security-Policy.
    • onAllReady (opsional): Callback yang diaktifkan saat semua proses rendering selesai, termasuk shell dan semua konten tambahan. Anda dapat menggunakan ini sebagai ganti onShellReady untuk crawler dan static generation. Jika Anda memulai streaming di sini, Anda tidak akan mendapatkan progressive loading apa pun. Stream akan berisi HTML final.
    • onError (opsional): Callback yang diaktifkan setiap kali ada kesalahan server, baik yang dapat dipulihkan atau tidak. Secara default, ini hanya memanggil console.error. Jika Anda menggantinya untuk mencatat laporan kerusakan, pastikan anda masih memanggil console.error. Anda juga dapat menggunakannya untuk menyesuaikan kode status sebelum shell di-emit.
    • onShellReady (opsional): Callback yang diaktifkan tepat setelah shell awal di-render. Anda dapat mengatur kode status dan memanggil pipe di sini untuk memulai streaming. React akan mengalirkan konten tambahan setelah shell bersama tag <script> inline yang menggantikan fallback pemuatan HTML dengan konten.
    • onShellError (opsional): Callback yang diaktifkan jika ada error saat me-render shell awal. Ini menerima error sebagai argumen. Belum ada bita yang di-emit dari stream. onShellReady maupun onAllReady tidak akan dipanggil. Dengan demikian, Anda dapat menampilkan shell HTML fallback.
    • progressiveChunkSize (opsional): Jumlah bita dalam sebuah chunk. Baca lebih lanjut tentang default heuristic.

Kembalian

renderToPipeableStream mengembalikan objek dengan dua metode:

  • pipe menghasilkan HTML dalam bentuk Writable Node.js Stream yang disediakan. Panggil pipe dalam onShellReady jika Anda ingin mengaktifkan streaming, atau panggil dalam onAllReady untuk crawler dan static generation.
  • abort memungkinkan Anda membatalkan server rendering dan me-render sisanya di sisi klien.

Penggunaan

Me-render pohon React sebagai HTML menjadi Node.js Stream

Panggil renderToPipeableStream untuk me-render pohon React Anda sebagai HTML menjadi Node.js Stream:

import { renderToPipeableStream } from 'react-dom/server';

// Sintaks route handler tergantung framework backend yang Anda gunakan
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});

Bersamaan dengan komponen root, Anda perlu menyediakan daftar path <script> bootstrap. 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>Aplikasi Saya</title>
</head>
<body>
<Router />
</body>
</html>
);
}

React akan menyisipkan doctype dan tag bootstrap <script> Anda ke dalam stream HTML yang dihasilkan:

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

Di sisi klien, skrip bootstrap Anda harus menghidrasi seluruh document dengan memanggil hydrateRoot:

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

hydrateRoot(document, <App />);

Ini akan melampirkan event listener untuk HTML yang dibuat server dan menjadikannya interaktif.

Deep Dive

Membaca path aset CSS and JS dari keluaran build

URL aset yang bersifat final (seperti file JavaScript and CSS) sering kali di-hash setelah proses build (pembangunan). Misalnya, alih-alih styles.css Anda mungkin mendapatkan styles.123456.css. Hashing nama file aset statis menjamin sebuah aset pada setiap build memiliki nama file yang berbeda. Ini berguna karena memungkinkan anda mengaktifkan caching jangka panjang dengan aman untuk aset statis: file dengan nama tertentu tidak akan mengubah konten.

Namun, jika Anda tidak mengetahui URL aset hingga selesainya proses build, tidak ada cara bagi Anda untuk memasukkan aset tersebut ke dalam source code. Misalnya, hardcoding "/styles.css" dalam JSX seperti sebelumnya tidak akan berfungsi. Untuk menjauhkan aset tersebut dari source code Anda, komponen Root dapat membaca nama file asli dari map yang dioper sebagai prop:

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

Di sisi server, render <App assetMap={assetMap} /> dan berikan assetMap URL aset:

// Anda perlu mendapatkan JSON ini dari build tooling yang Anda gunakan, misalnya dari keluaran build.
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);
}
});
});

Karena server Anda sekarang me-render <App assetMap={assetMap} />, Anda juga perlu me-render assetMap di sisi klien untuk menghindari error hidrasi. Anda dapat melakukan serialisasi dan mengoper assetMap kepada klien seperti ini:

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

app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// Hati-hati: di sini aman menggunakan stringify() karena data ini tidak dibuat oleh pengguna.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});

Pada contoh di atas, opsi bootstrapScriptContent menambahkan tag <script> inline ekstra yang mengatur variabel global window.assetMap 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} />);

Baik klien maupun server me-render 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, perhatikan halaman profil yang menampilkan sampul, sidebar dengan daftar teman dan foto, dan daftar postingan:

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

Bayangkan bahwa memuat data <Posts /> membutuhkan waktu. Idealnya, Anda ingin menampilkan konten lain dari halaman profil kepada pengguna tanpa perlu menunggu daftar postingan. Untuk melakukannya, letakkan Posts dalam <Suspense> boundary:

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 fallback pemuatan (PostsGlimmer) terlebih dahulu, kemudian ketika Posts selesai memuat datanya, React akan mengirimkan HTML yang tersisa bersama dengan tag <script> inline yang menggantikan fallback pemuatan dengan HTML itu. Dari perspektif pengguna, halaman pertama kali akan tampil dengan PostsGlimmer, kemudian digantikan dengan Posts.

Anda dapat membuat <Suspense> boundary bersarang sehingga urutan pemuatan lebih terperinci:

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

Pada contoh di atas, React dapat memulai streaming halaman lebih awal. Hanya ProfileLayout dan ProfileCover yang harus menyelesaikan rendering terlebih dahulu karena tidak berada dalam <Suspense> boundary. Apabila Sidebar, Friends, atau Photos perlu memuat data, React akan mengirimkan HTML fallback BigSpinner terlebih dahulu. Kemudian, ketika lebih banyak data tersedia, lebih banyak konten yang ditampilkan hingga semuanya terlihat.

Streaming tidak perlu menunggu React sendiri dimuat di peramban maupun menunggu aplikasi Anda menjadi interaktif. Konten HTML dari server akan ditampilkan secara bertahap sebelum tag <script> mana pun dimuat.

Baca selengkapnya tentang cara kerja streaming HTML.

Note

Hanya sumber data yang mendukung Suspense yang akan mengaktifkan komponen Suspense. Di antaranya:

  • Data fetching dengan framework yang mendukung Suspense seperti Relay dan Next.js
  • Pemuatan kode komponen secara lazy-loading dengan lazy

Suspense tidak dapat medeteksi data fetching jika dilakukan dalam Effect atau event handler.

Cara persis pemuatan data dalam komponen Posts di atas tergantung framework yang Anda gunakan. Jika Anda menggunakan framework yang mendukung Suspense, Anda dapat menemukan detailnya dalam dokumentasi framework tersebut tentang data fetching.

Data fetching secara Suspense-enabled tanpa menggunakan framework yang opinionated masih belum didukung. Persyaratan untuk mengimplementasikan sumber data yang mendukung Suspense masih belum stabil dan belum terdokumentasi. API resmi untuk mengintegrasikan sumber data dengan Suspense akan dirilis dalam versi React yang akan datang.


Menentukan apa yang masuk ke dalam shell

Bagian aplikasi Anda di luar <Suspense> disebut shell (cangkang):

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

Ini menentukan status pemuatan paling awal yang dapat dilihat pengguna:

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

Jika Anda memasukkan seluruh cakupan aplikasi di level root ke dalam <Suspense>, shell hanya akan berisi spinner tersebut. Ini bukan pengalaman pengguna yang menyenangkan karena melihat spinner besar di layar dapat terasa lebih lambat dan lebih menyebalkan daripada menunggu sedikit lebih lama dan melihat tata letak yang sebenarnya. Inilah alasan mengapa biasanya Anda ingin menempatkan batas <Suspense> sehingga shell terasa minimal tetapi lengkap—seperti kerangka dari keseluruhan tata letak halaman.

Callback onShellReady diaktifkan ketika seluruh shell telah di-render. Biasanya, Anda akan mulai melakukan streaming:

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});

Pada saat onShellReady diaktifkan, komponen di dalam <Suspense> bersarang mungkin masih memuat data.


Mencatat laporan kerusakan di server

Secara default, semua error di server dicatat di konsol. Anda dapat mengganti perilaku ini untuk mencatat laporan kerusakan:

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});

Jika Anda menyediakan implementasi onError khusus, jangan lupa juga untuk mencatat kesalahan di konsol seperti di atas.


Memulihkan dari error di dalam shell

Pada contoh berikut, shell berisi ProfileLayout, ProfileCover, dan PostsGlimmer:

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

Jika terjadi error saat me-render komponen tersebut, React tidak akan memiliki HTML yang berarti untuk dikirim ke klien. Ganti onShellError untuk mengirim HTML fallback yang tidak bergantung pada server rendering sebagai upaya terakhir:

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

Jika ada error saat membuat shell, onError dan onShellError akan diaktifkan. Gunakan onError untuk pelaporan kesalahan dan gunakan onShellError untuk mengirim dokumen HTML cadangan. HTML fallback Anda tidak harus berupa halaman error. Sebagai gantinya, Anda dapat menyertakan shell alternatif yang me-render aplikasi Anda hanya pada klien.


Memulihkan dari error di luar shell

Pada contoh berikut, komponen <Posts /> berada di dalam <Suspense> sehingga bukan merupakan bagian dari shell:

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

Jika error terjadi pada komponen Posts atau suatu tempat di dalamnya, React akan mencoba memulihkannya:

  1. Mengirimkan fallback pemuatan dari <Suspense> boundary terdekat (PostsGlimmer) ke HTML halaman.
  2. “Menyerah” untuk mencoba me-render konten Posts di server lagi.
  3. Saat kode JavaScript dimuat di klien, React akan mencoba lagi me-render Posts dari sisi klien.

Jika percobaan me-render ulang Posts dari klien juga gagal, React akan melempar kesalahan di klien. Seperti halnya semua error yang terjadi selama rendering, error boundary induk terdekat menentukan bagaimana error ditampilkan kepada pengguna. Dalam praktiknya, ini berarti bahwa pengguna akan melihat indikator pemuatan hingga dipastikan bahwa error tidak dapat dipulihkan.

Jika percobaan me-render ulang Posts dari klien berhasil, fallback pemuatan dari server akan diganti dengan keluaran rendering klien. Pengguna tidak akan tahu bahwa ada kesalahan server. Namun, callback onError server dan callback onRecoverableError klien akan aktif sehingga Anda bisa mendapatkan pemberitahuan tentang error tersebut.


Mengatur kode status

Streaming menimbulkan tradeoff (pengorbanan). Anda ingin memulai streaming halaman sedini mungkin agar pengguna dapat melihat konten lebih cepat. Namun, begitu Anda memulai streaming, Anda tidak dapat lagi menyetel kode status respons.

Dengan membagi aplikasi Anda ke dalam shell (di atas semua batas <Suspense>) dan konten lainnya, Anda telah menyelesaikan sebagian dari masalah ini . Jika shell terjadi error, Anda akan mendapatkan callback onShellError yang memungkinkan Anda menyetel kode status error. Jika tidak, Anda tahu bahwa aplikasi dapat pulih pada klien, sehingga Anda dapat mengirimkan “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>Ada yang salah</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});

Jika sebuah komponen di luar shell (yaitu di dalam <Suspense> boundary) melontarkan error, React tidak akan berhenti me-render. Ini berarti callback onError akan diaktifkan, tetapi Anda masih akan mendapatkan onShellReady alih-alih onShellError. Ini karena React akan mencoba memulihkan dari error itu dari sisi klien, seperti yang dijelaskan di atas.

Namun, jika mau, Anda dapat menggunakan fakta bahwa ada kesalahan untuk menyetel kode status:

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>Ada yang salah</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});

Ini hanya akan menangkap error di luar shell yang terjadi saat membuat konten shell awal, jadi tidak lengkap. Jika mengetahui apakah terjadi error untuk beberapa konten sangat penting, Anda dapat memindahkannya ke dalam shell.


Menangani error yang berbeda dengan cara yang berbeda

Anda dapat membuat subclass Error sendiri dan menggunakan operator instanceof untuk memeriksa kesalahan mana yang dilontarkan. Misalnya, Anda dapat menentukan NotFoundError khusus dan melontarkannya dari komponen Anda. Kemudian callback onError, onShellReady, dan onShellError dapat melakukan sesuatu yang berbeda bergantung pada jenis kesalahan:

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>Ada yang salah</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});

Perlu diingat bahwa setelah Anda meng-emit shell dan memulai streaming, Anda tidak dapat mengubah kode status.


Menunggu semua konten dimuat untuk crawler dan static generation

Streaming menawarkan pengalaman pengguna yang lebih baik karena pengguna dapat melihat konten saat tersedia.

Namun, saat crawler mengunjungi halaman Anda, atau jika Anda membuat halaman pada saat proses pembangunan, Anda mungkin ingin membiarkan semua konten dimuat terlebih dahulu, lalu menghasilkan keluaran akhir HTML alih-alih menampilkannya secara bertahap.

Anda dapat menunggu semua konten dimuat menggunakan callback 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);
}
});

Pengunjung reguler akan mendapatkan stream konten yang dimuat secara progresif. Crawler akan menerima keluaran akhir HTML setelah semua data dimuat. Namun, ini juga berarti crawler harus menunggu semua data, beberapa di antaranya mungkin lambat dimuat atau error. Bergantung pada aplikasi Anda, Anda juga dapat memilih untuk mengirim shell ke crawler.


Membatalkan server rendering

Anda dapat memaksa server rendering untuk “menyerah” setelah timeout:

const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});

setTimeout(() => {
abort();
}, 10000);

React akan menghapus fallback pemuatan yang tersisa sebagai HTML, dan akan mencoba me-render sisanya dari sisi klien.