renderToReadableStream
renderToReadableStream, bir React tree’yi bir Readable Web Stream olarak render eder.
const stream = await renderToReadableStream(reactNode, options?)- Referans
- Kullanım
- React tree’ini HTML olarak bir Readable Web Stream içine render etmek
- İçerik yüklendikçe stream etmek
- Shell’e nelerin dahil edileceğini belirtmek
- Server’da crash’leri loglamak
- Shell içindeki hatalardan kurtulmak
- ### Shell dışındaki hatalardan kurtulmak
- ### Status kodunu ayarlamak
- ### Farklı hataları farklı şekillerde işlemek
- ### Crawlers ve statik üretim için tüm içeriğin yüklenmesini beklemek
- Server render’ını iptal etmek
Referans
renderToReadableStream(reactNode, options?)
renderToReadableStream fonksiyonunu çağırarak React tree’nizi HTML olarak bir Readable Web Stream içine render edebilirsiniz.
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' },
});
}Client tarafında, server tarafından oluşturulan HTML’i interaktif hale getirmek için hydrateRoot fonksiyonunu çağırın.
Daha fazla örneği aşağıda inceleyin.
Parametreler
-
reactNode: HTML’e render etmek istediğiniz bir React node. Örneğin<App />gibi bir JSX element’i. Bunun tüm dokümanı temsil etmesi beklenir, bu yüzdenAppcomponent’i<html>etiketini render etmelidir. -
opsiyonel
options: Stream için yapılandırma seçeneklerini içeren bir obje.- opsiyonel
bootstrapScriptContent: Belirtilirse, bu string inline bir<script>etiketi içinde yer alır. - opsiyonel
bootstrapScripts: Sayfada emit edilecek<script>etiketleri için string URL’lerden oluşan bir dizi.hydrateRootfonksiyonunu çağıran<script>’i eklemek için bunu kullanın. React’in client tarafında çalışmasını istemiyorsanız bunu boş bırakın. - opsiyonel
bootstrapModules:bootstrapScriptsgibi, ancak<script type="module">olarak emit eder. - opsiyonel
identifierPrefix:useIdtarafından üretilen ID’ler için React’in kullandığı string önek. Aynı sayfada birden fazla root kullanırken çakışmaları önlemek için faydalıdır.hydrateRootile verilen önek ile aynı olmalıdır. - opsiyonel
namespaceURI: Stream için root namespace URI string’i. Varsayılan olarak normal HTML. SVG için'http://www.w3.org/2000/svg'veya MathML için'http://www.w3.org/1998/Math/MathML'geçebilirsiniz. - opsiyonel
nonce:script-srcContent-Security-Policy için script’lere izin vermek amacıyla birnoncestring’i. - opsiyonel
onError: Server’da bir hata oluştuğunda tetiklenen callback. Hata recoverable veya non-recoverable olabilir. Varsayılan olarak sadececonsole.errorçağrılır. Bunu crash raporlarını loglamak için override ederseniz, yine deconsole.errorçağırdığınızdan emin olun. Ayrıca shell emit edilmeden önce status kodunu ayarlamak için de kullanabilirsiniz. - opsiyonel
progressiveChunkSize: Bir chunk içindeki byte sayısı. Varsayılan heuristic hakkında daha fazla bilgi. - opsiyonel
signal: abort signal ile server render’ı abort edebilir ve kalan kısmı client’ta render edebilirsiniz.
- opsiyonel
Returns
renderToReadableStream returns a Promise:
- Eğer shell render’ı başarılı olursa, bu Promise bir Readable Web Stream ile çözülür.
- Eğer shell render’ı başarısız olursa, Promise reddedilir. Bunu bir fallback shell göstermek için kullanabilirsiniz.
Return stream’in ek bir özelliği vardır:
allReady: Tüm render işlemi tamamlandığında çözülen bir Promise, bu hem shell hem de tüm ek içerik için geçerlidir. Response döndürmeden önceawait stream.allReadyyapabilirsiniz crawlers ve statik üretim için. Eğer bunu yaparsanız, progressive loading almazsınız. Stream, final HTML’i içerir.
Kullanım
React tree’ini HTML olarak bir Readable Web Stream içine render etmek
React tree’nizi HTML olarak bir Readable Web Stream içine render etmek için renderToReadableStream fonksiyonunu çağırın.
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' },
});
}Root component ile birlikte bir bootstrap <script> yolları listesi sağlamanız gerekir.. Root component’iniz tüm dokümanı, root <html> etiketi dahil olmak üzere döndürmelidir.
Örneğin, şöyle görünebilir:
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 ve bootstrap <script> etiketlerini oluşan HTML stream’ine enjekte edecektir:
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>Client tarafında, bootstrap script’iniz tüm document’i hydrateRoot çağrısı ile hydrate etmelidir:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);Bu server tarafından oluşturulan HTML’e event listener’lar ekleyecek ve HTML’i interaktif hale getirecektir.
Derinlemesine İnceleme
Final asset URL’leri (JavaScript ve CSS dosyaları gibi) genellikle build sonrası hash’lenir. Örneğin, styles.css yerine styles.123456.css gibi bir dosya ile karşılaşabilirsiniz. Statik asset dosya adlarını hash’lemek, aynı asset’in her farklı build’inin farklı bir dosya adına sahip olmasını garanti eder. Bu faydalıdır çünkü statik assetler için uzun süreli caching’i güvenle etkinleştirmenizi sağlar: belli bir isimdeki dosyanın içeriği hiçbir zaman değişmez.
Ancak, asset URL’lerini build sonrası öğreniyorsanız, bunları kaynak kodunuza koymanın bir yolu yoktur. Örneğin, daha önce JSX içine hardcode edilmiş "/styles.css" çalışmaz. Bunları kaynak kodunuzdan çıkarmak için, root component’iniz gerçek dosya adlarını bir prop olarak geçirilen bir map’ten okuyabilir:
export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}Server tarafında, <App assetMap={assetMap} /> render edin ve asset URL’lerini içeren assetMap’inizi geçin:
// Bu JSON’u build aracınızdan almanız gerekir, örneğin build çıktısından okuyabilirsiniz.
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' },
});
}Server artık <App assetMap={assetMap} /> render ettiği için, client tarafında da assetMap ile render etmeniz gerekir; aksi takdirde hydration hataları oluşur. assetMap’i serialize edip client’a şöyle geçirebilirsiniz:
// Bu JSON’u build aracınızdan almanız gerekir.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Dikkat: Bunu stringify() ile güvenle dönüştürebilirsiniz çünkü bu veri kullanıcı tarafından üretilmiş değil.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}Yukarıdaki örnekte, bootstrapScriptContent seçeneği client tarafında global window.assetMap değişkenini ayarlayan ekstra bir inline <script> etiketi ekler. Bu sayede client kodu aynı assetMap’i okuyabilir:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);Hem client hem de server, App’i aynı assetMap prop’u ile render eder; böylece herhangi bir hydration hatası oluşmaz.
İçerik yüklendikçe stream etmek
Streaming, kullanıcının tüm veri server’da yüklenmeden önce içeriği görmeye başlamasını sağlar. Örneğin, bir profil sayfasını düşünün; burada bir kapak fotoğrafı, arkadaşlar ve fotoğraflar ile dolu bir yan panel ve bir gönderi listesi gösterilmektedir:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}Diyelim ki <Posts /> için veri yüklemesi biraz zaman alıyor. İdeal olarak, gönderileri beklemeden kullanıcıya profil sayfasının geri kalanını göstermek istersiniz. Bunu yapmak için, Posts’i bir <Suspense> sınırına sarın:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}Bu, React’e Posts verilerini yüklemeden önce HTML’i stream etmeye başlamasını söyler. React önce loading fallback (PostsGlimmer) için HTML’i gönderir, ardından Posts verilerini yüklemeyi bitirdiğinde kalan HTML’i ve loading fallback’i bu HTML ile değiştiren inline bir <script> etiketi gönderir. Kullanıcı perspektifinden bakıldığında, sayfa önce PostsGlimmer ile görünür, sonra Posts ile değiştirilir.
Daha ayrıntılı bir yükleme sırası oluşturmak için nested <Suspense> sınırları kullanabilirsiniz:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}Bu örnekte, React sayfayı çok daha erken stream etmeye başlayabilir. Sadece ProfileLayout ve ProfileCover öncelikle render’ı tamamlamalıdır, çünkü bunlar herhangi bir <Suspense> sınırına sarılmamıştır. Ancak, Sidebar, Friends veya Photos veri yüklemesi gerektiriyorsa, React bunun yerine BigSpinner fallback HTML’ini gönderir. Daha fazla veri kullanılabilir hale geldikçe, içerik kademeli olarak gösterilmeye devam eder.
Streaming, React’in browser’da yüklenmesini veya uygulamanızın interaktif hale gelmesini beklemek zorunda değildir. Server’dan gelen HTML içeriği, herhangi bir <script> yüklenmeden önce kademeli olarak açığa çıkar.
Streaming HTML’in nasıl çalıştığı hakkında daha fazla bilgi edinin.
Shell’e nelerin dahil edileceğini belirtmek
Herhangi bir <Suspense> sınırının dışında kalan uygulama kısmına shell denir:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}Bu, kullanıcının görebileceği en erken yükleme durumunu belirler:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>Eğer tüm uygulamayı root’ta bir <Suspense> sınırına sararsanız, shell sadece o spinner’ı içerir. Ancak bu hoş bir kullanıcı deneyimi değildir çünkü ekranda büyük bir spinner görmek, biraz daha bekleyip gerçek layout’u görmekten daha yavaş ve can sıkıcı gelebilir. Bu yüzden genellikle <Suspense> sınırlarını, shell’in minimal ama tamamlanmış hissettirecek şekilde yerleştirirsiniz—tüm sayfa layout’unun bir iskeleti gibi.
renderToReadableStream asenkron çağrısı, tüm shell render edildikten hemen sonra bir stream ile çözülecektir. Genellikle, o zaman streaming’i başlatır ve o stream ile bir response oluşturup döndürürsünüz:
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}stream döndürüldüğü sırada, iç içe geçmiş <Suspense> sınırları içindeki component’ler hâlâ veri yüklüyor olabilir.
Server’da crash’leri loglamak
Varsayılan olarak, server’daki tüm hatalar console’a loglanır. Bu davranışı override ederek crash raporlarını loglayabilirsiniz:
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' },
});
}Özel bir onError implementasyonu sağlarsanız, yukarıdaki gibi hataları console’a da loglamayı unutmayın.
Shell içindeki hatalardan kurtulmak
Bu örnekte, shell ProfileLayout, ProfileCover ve PostsGlimmer içerir:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}Eğer bu component’ler render edilirken bir hata oluşursa, React’in client’a gönderebileceği anlamlı bir HTML olmayacaktır. Son çare olarak server render’a bağlı olmayan bir fallback HTML göndermek için renderToReadableStream çağrınızı bir try...catch bloğuna sarın:
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' },
});
}
}Eğer shell oluşturulurken bir hata oluşursa, hem onError hem de catch bloğunuz tetiklenir. Hata raporlaması için onError’u, fallback HTML göndermek için ise catch bloğunu kullanın. Fallback HTML’inizin bir hata sayfası olması gerekmez. Bunun yerine, uygulamanızı sadece client tarafında render eden alternatif bir shell içerebilirsiniz.
### Shell dışındaki hatalardan kurtulmak
Bu örnekte, <Posts /> component’i <Suspense> ile sarılmıştır, bu yüzden shell’in bir parçası değildir:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}Eğer Posts component’inde veya içindeki bir yerde hata oluşursa, React bundan kurtulmayı deneyecektir:
- En yakın
<Suspense>sınırı (PostsGlimmer) için loading fallback HTML’e emit edilir. - Server üzerinde
Postsiçeriğini render etmeye çalışmaktan vazgeçer. - JavaScript kodu client’ta yüklendiğinde, React
Posts’i client üzerinde yeniden deneyecektir.
Eğer client üzerinde Posts render’ını yeniden denemek de başarısız olursa, React hatayı client’ta fırlatır. Render sırasında oluşan tüm hatalarda olduğu gibi, en yakın parent error boundary hatayı kullanıcıya nasıl göstereceğinizi belirler. Pratikte, kullanıcı, hatanın geri döndürülemez olduğundan emin olunana kadar bir loading göstergesi görür.
Eğer client üzerinde Posts render’ını yeniden denemek başarılı olursa, server’dan gelen loading fallback client render çıktısı ile değiştirilir. Kullanıcı server hatasının farkına varmaz. Ancak, server onError callback’i ve client onRecoverableError callback’leri tetiklenir, böylece hatadan haberdar olabilirsiniz.
### Status kodunu ayarlamak
Streaming bir takas getirir. Sayfanın içeriğini kullanıcıya daha erken gösterebilmek için mümkün olan en erken zamanda streaming başlatmak istersiniz. Ancak streaming başladıktan sonra response status kodunu artık ayarlayamazsınız.
Uygulamanızı shell (tüm <Suspense> sınırlarının üstünde) ve kalan içerik olarak böldüğünüzde, bu sorunun bir kısmını çözmüş olursunuz. Eğer shell hata verirse, catch bloğunuz çalışır ve hata status kodunu ayarlamanıza imkan tanır. Aksi takdirde, uygulamanın client’ta kurtulabileceğini bildiğiniz için “OK” gönderebilirsiniz.
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' },
});
}
}Eğer bir component shell’in dışında (yani bir <Suspense> sınırı içinde) hata fırlatırsa, React render’ı durdurmaz. Bu, onError callback’inin tetikleneceği anlamına gelir, ancak kodunuz catch bloğuna girmeden çalışmaya devam eder. Bunun nedeni, React’in bu hatadan client üzerinde kurtulmayı denemesi, yukarıda açıklandığı gibi.
Ancak isterseniz, bir şeyin hata verdiğini kullanarak status kodunu ayarlayabilirsiniz:
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' },
});
}
}Bu, yalnızca başlangıç shell içeriği oluşturulurken meydana gelen shell dışındaki hataları yakalar; dolayısıyla kapsamlı değildir. Eğer bazı içerikler için bir hata oluşup oluşmadığını bilmek kritikse, o içeriği shell içine taşıyabilirsiniz.
### Farklı hataları farklı şekillerde işlemek
Kendi Error alt sınıflarınızı oluşturabilir ve hangi hatanın fırlatıldığını kontrol etmek için instanceof operatörünü kullanabilirsiniz. Örneğin, özel bir NotFoundError tanımlayıp component’inizden fırlatabilirsiniz. Ardından hatayı onError içinde kaydedebilir ve hata türüne bağlı olarak response döndürmeden önce farklı bir işlem yapabilirsiniz:
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' },
});
}
}Unutmayın: Shell’i emit edip streaming’e başladıktan sonra status kodunu değiştiremezsiniz.
### Crawlers ve statik üretim için tüm içeriğin yüklenmesini beklemek
Streaming, kullanıcı içeriği kullanılabilir hale geldikçe görebildiği için daha iyi bir kullanıcı deneyimi sunar.
Ancak, bir crawler sayfanızı ziyaret ettiğinde veya sayfaları build zamanında üretiyorsanız, tüm içeriğin önce yüklenmesini ve ardından final HTML çıktısının üretilmesini isteyebilirsiniz; kademeli olarak açığa çıkarmak yerine.
Tüm içeriğin yüklenmesini, stream.allReady Promise’ini await ederek bekleyebilirsiniz:
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' },
});
}
}Normal bir ziyaretçi, kademeli olarak yüklenen içeriğin bir stream’ini alır. Bir crawler ise tüm veri yüklendikten sonra final HTML çıktısını alır. Ancak bu, crawler’ın tüm veriyi beklemesi gerektiği anlamına gelir; bazıları yavaş yüklenebilir veya hata verebilir. Uygulamanıza bağlı olarak, shell’i crawler’lara da göndermeyi tercih edebilirsiniz.
Server render’ını iptal etmek
Server render’ının belirli bir süre sonra “vazgeçmesini” sağlayabilirsiniz:
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, kalan loading fallback’ları HTML olarak flush eder ve geri kalan içeriği client üzerinde render etmeye çalışır.