renderToReadableStream
renderToReadableStream
renderiza un arbol de React a Readable Web Stream.
const stream = await renderToReadableStream(reactNode, options?)
- Referencia
- Uso
- Renderizar un árbol de React como HTML a un Readable Web Stream
- Streamear más contenido mientras carga
- Especificar qué va dentro del shell
- Registrando fallos en el servidor
- Recuperar errores dentro del shell
- Recuperar errores fuera del shell
- Establecer el código de estado
- Gestionar distintos errores de distintas maneras
- Esperar a que se cargue todo el contenido para rastreadores y generación estática
- Abortar el renderizado en el servidor
Referencia
renderToReadableStream(reactNode, options?)
Llama a la función renderToReadableStream
para renderizar tu árbol de React como HTML a un legible stream para web
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' },
});
}
Desde el cliente, llama a hydrateRoot
para hacer interactivo el HTML generado en el servidor.
Parámetros
-
reactNode
: Es un nodo de React que quieres renderizar en HTML. Por ejemplo, un elemento JSX como<App />
. Se presupone que representará el documento completo, por lo que el componenteApp
debería renderizar la etiqueta<html>
. -
opcional
options
: Objeto con las opciones de streaming.- opcional
bootstrapScriptContent
: Si se especifica, esta cadena será colocada dentro de una etiqueta<script>
. - opcional
bootstrapScripts
: Array de strings que representan los URL para que la etiqueta<script>
las emita en la página. Utiliza esto para incluír el<script>
que llama ahydrateRoot
. Omítelo si no planeas usar React en el cliente. - opcional
bootstrapModules
: Igual quebootstrapScripts
, pero emite<script type="module">
en su lugar. - opcional
identifierPrefix
: Prefijo (string) que React usa para los ID generados medianteuseId
. Útil para evitar conflictos cuando se usan múltiples raíces (roots) en la misma página. Debe ser el mismo prefijo que se le pasa ahydrateRoot
. - opcional
namespaceURI
: Un string con la raíz del namespace URI para el stream. Por defecto se usa HTML. Usa'http://www.w3.org/2000/svg'
para SVG o'http://www.w3.org/1998/Math/MathML'
para MathML. - opcional
nonce
: Un stringnonce
que le permite al script lascript-src
política de contenido seguro (CSP). - opcional
onError
: Callback que se activa cuando ocurre un error en el servidor, sea recuperable o no. Por defecto, solo ejecutaconsole.error
. Si lo sobreescribes para registrar errores en el servidor, asegúrate que sigas llamando aconsole.error
. También puedes usarlo para ajustar el código de estado antes de que el shell sea emitido. - opcional
progressiveChunkSize
: El número de bytes en un chunk. Leer más sobre el heurístico que se usa por defecto. - opcional
signal
: Una señal de aborto que permite abortar el renderizado en el servidor y el resto del renderizado en el cliente.
- opcional
Devuelve
renderToReadableStream
devuelve una promesa:
- Si el shell se renderizó satisfactoriamente, la promesa se devolverá a un Readable Web Stream.
- Si el renderizado del shell falla, la promesa será rechazada. Usa esto para mostrar un shell mediante un callback.
El stream devuelto tiene una propiedad adicional:
allReady
: Promesa que se resuelve cuando todo el renderizado se ha completado, incluyendo tanto el shell como todo el contenido adicional. Puedes esperar aawait stream.allReady
antes de devolver una respuesta para rastreadores y generación estática. Si usas esto, no obtendrás carga progresiva. El stream contendrá el HTML final.
Uso
Renderizar un árbol de React como HTML a un Readable Web Stream
Llama renderToReadableStream
para renderizar tu árbol de React como HTML a 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' },
});
}
Junto al componente raíz, necesitas proporcionar una lista de rutas de <script>
de arranque. Tu componente raíz debería devolver el documento completo, incluyendo la etiqueta <html>
.
Por ejemplo, debería verse de esta forma:
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>Mi app</title>
</head>
<body>
<Router />
</body>
</html>
);
}
React inyectará el doctype y tus etiquetas de <script>
de arranque dentro del HTML resultante del stream:
<!DOCTYPE html>
<html>
<!-- ... HTML de tus componentes ... -->
</html>
<script src="/main.js" async=""></script>
En el cliente, tu script inicial debería hidratar el document
entero con una llamada a hydrateRoot
:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
Esto adjuntará detectores de eventos al HTML generado en el servidor, haciendo que este sea interactivo.
Profundizar
Las URL finales de los recursos (como los archivos CSS y JS) suelen ser hasheadas después de la compilación. Por ejemplo, en lugar de styles.css
podrías tener styles.123456.css
. El hasheo de nombres de archivos estáticos garantiza que cada versión distinta del mismo recurso tendrá un nombre de archivo diferente. Esto es útil porque te permite activar de forma segura el almacenamiento en caché a largo plazo para recursos estáticos: un archivo con un nombre característico nunca cambiaría su contenido.
Sin embargo, si no conoces los URL de los recursos hasta después de tener el build, no te será posible ponerlas en el código fuente. Por ejemplo, si hardcodeas "/styles.css"
en JSX, esto no funcionaría, puesto que es una URL relativa. Para mantenerlas fuera del código fuente, tu componente raíz puede leer el nombre real de un archivo a través de un mapa pasado como propiedad:
export default function App({ assetMap }) {
return (
<html>
<head>
<title>Mi app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}
En el servidor, renderiza <App assetMap={assetMap} />
y pasa tu assetMap
con los URL de los recursos:
// Necesitarás obtener este JSON a través de tus herramientas de compilación. Por ejemplo, leyéndolo desde la consola de compilación
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' },
});
}
Dado que es el servidor quien está renderizando <App assetMap={assetMap} />
, necesitarás renderizarlo con assetMap
en el cliente también para evitar errores de hidratación. Puedes serializar y pasar assetMap
al cliente de esta forma:
// Necesitarás obtener este JSON a través de tus herramientas de compilación.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Precaución: Es seguro usar stringify() aquí porque esta data no ha sido generada por el usuario.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
En el ejemplo de arriba, la opción bootstrapScriptContent
añade una etiqueta <script>
extra que establece la variable global window.assetMap
en el cliente. Esto le permite al código en el cliente leer el mismo assetMap
:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
Tanto el cliente como el servidor renderizan App
con la misma propiedad assetMap
, por lo que no da lugar a errores de hidratación.
Streamear más contenido mientras carga
Streamear le permite al usuario empezar a ver el contenido incluso antes de que se haya cargado del todo en el servidor. Como ejemplo, usaremos una página de perfil que muestra una cabecera, un menú lateral con amigos e imágenes, y una lista de publicaciones:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}
Imagina que cargar la información de <Posts />
toma un tiempo. Lo ideal sería mostrar al usuario el resto del perfil sin tener que esperar a que carguen las publicaciones. Para lograr esto, envuelve el componente Posts
dentro de <Suspense>
:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Esto le indica a React que comienze a streamear el HTML antes de que Posts
cargue su información. React enviará primero el HTML del componente de carga (PostsGlimmer
) y, después, cuando Posts
termine de cargar su información, React enviará el HTML restante junto a una etiqueta <script>
que reemplazará el HTML de carga enviado previamente por el nuevo contenido. Desde la perspectiva del usuario, la página aparecerá primero con el contenido de PostsGlimmer
, y después será reemplazado por el de Posts
.
Se pueden anidar componentes <Suspense>
para generar una secuencia de carga más granular:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
En este ejemplo, React puede empezar a streamear el contenido mucho antes. Solo ProfileLayout
y ProfileCover
deben terminar de renderizar primero porque no están envueltos en ningún componente <Suspense>
. Sin embargo, si Sidebar
, Friends
, o Photos
necesitan cargar información, React enviará el HTML de BigSpinner
en su lugar. Después, a medida que más información se vuelve disponible, se irá revelando el contenido hasta que toda la página haya cargado.
Cuando se streamea no es necesario esperar a que React en sí mismo cargue en el navegador o a que tu aplicación se vuelva interactiva. El contenido HTML irá siendo revelado progresivamente antes de que cualquier etiqueta <script>
cargue.
Lee más sobre cómo funciona el streaming de HTML.
Especificar qué va dentro del shell
La parte de tu app que va fuera de cualquier envoltura <Suspense>
es conocida como el shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
Determina el primer estado de carga que el usuario podrá ver:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>
Si envuelves toda la app dentro del componente <Suspense>
en la raíz, el shell solo contendrá el círculo de carga. No obstante, esto generaría una experiencia de usuario poco agradable, ya que ver un símbolo de carga gigante puede hacer que la app se sienta lenta y es más molesto que esperar un poco más y ver la disposición real de la página. Es por esto que es conveniente utilizar el componente <Suspense>
de forma que en el shell se vea el contenido mínimo, pero que esté completo—como si fuera el esqueleto de la disposición de la página.
La llamada asíncrona a renderToReadableStream
será resuelta a un stream
tan pronto como el shell haya sido renderizado. Entonces, empezarás a streamear creando y devolviendo una respuesta con este stream
:
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
Para cuando el stream
sea devuelto, los componentes anidados en <Suspense>
podrían estar aún cargando la información.
Registrando fallos en el servidor
Por defecto, todos los errores que ocurren en el servidor son registrados en la consola. Es posible sobreescribir esto para generar informes de fallo:
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' },
});
}
Si se provee una implementación personalizada de onError
, no olvides registrar también los errores en la consola, como hacemos en el ejemplo superior.
Recuperar errores dentro del shell
En este ejemplo, el shell contiene ProfileLayout
, ProfileCover
y PostsGlimmer
:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Si ocurriera un error al renderizar estos componentes, React no tendría ningún HTML significativo que enviar al cliente. Envuelve la llamada a renderToReadableStream
en un bloque try...catch
para enviar HTML alternativo que no depende del renderizado en el servidor como último recurso:
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' },
});
}
}
Si ocurriera un error al generar el shell, tanto onError
como el bloque catch
serían disparados. Usa onError
para reportar el error y el bloque catch
para enviar el HTML alternativo. El HTML alternativo no tiene necesariamente por qué ser un mensaje de error. En su lugar, puedes incluír un shell alternativo que renderiza la app solo en el cliente.
Recuperar errores fuera del shell
En este ejemplo, el componente <Posts />
está envuelto en <Suspense>
, por lo que no es parte del shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Si ocurriera un error en el componente Posts
o en cualquier parte dentro del mismo, React intentará recuperse de él:
- Emitirá el respaldo de carga del
<Suspense>
más cercano (PostsGlimmer
) en el HTML. - Dejará de intentar renderizar el contenido de
Posts
en el servidor. - Cuando el código de JavaScript cargue en el cliente, React intentará renderizar
Posts
en el cliente.
Si el intento de cargar Posts
desde el cliente también falla, React lanzará el error en el cliente. Igual que con el resto de errores lanzados durante el renderizado, el límite de error más cercano determinará cómo mostrar el error al usuario. En la práctica, esto significa que el usuario verá un símbolo de carga hasta que el error sea imposible de recuperar.
Si el intento de renderizado de Posts
en el cliente se logra con éxito, el símbolo de carga recibido desde el servidor será reemplazado con el HTML obtenido como resultado. El usuario no sabrá que ocurrió un error en el servidor. Sin embargo, la función onError
del servidor y la función onRecoverableError
del cliente serán disparadas, por lo que podrás ser notificado/a del error.
Establecer el código de estado
El streaming requiere de una compensación. Se pretende empezar el streaming lo antes posible de forma que el usuario vea el contenido lo antes posible también. No obstante, una vez se empieza el streaming, no es posible establecer un código de estado.
Al dividir la app en el shell (por encima de los perímetros <Suspense>
) y el resto de contenido, ya se ha solucionado una parte de este problema. Si el shell falla, el bloque catch
te permitirá establecer el código de estado del error. De lo contrario, sabes que la app se podrá recuperar en el cliente, por lo que puedes enviar un «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' },
});
}
}
Si un componente fuera del shell (por ejemplo, dentro del perímetro <Suspense>
) lanza un error, React no dejará de renderizar. Esto significa que la función onError
será disparada, pero tu código continuará ejecutándose sin entrar en el bloque catch
. Esto es porque React tratará de recuperarse de este error en el cliente, como hemos descrito previamente.
Sin embargo, si lo deseas, puedes usar el hecho de que algo ha fallado para establecer el código de error:
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' },
});
}
}
Esto solo capturará errores de fuera del shell que ocurrieron mientras se generaba el contenido inicial del shell, por lo que no es exhaustivo. Si saber si ocurrió un error es fundamental para algún contenido, puede moverlo hacia arriba en el shell.
Gestionar distintos errores de distintas maneras
Puedes crear tus propias subclases de Error
y utilizar el operador instanceof
para comprobar qué error ha sido lanzado. Por ejemplo, puedes definir un NotFoundError
personalizado y lanzarlo desde tu componente. Después, puedes guardar el error dentro de la función onError
y hacer algo distinto antes de devolver la respuesta dependiendo del tipo de error:
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' },
});
}
}
Ten en cuenta que una vez emites el shell y empiezas a streamear, no podrás cambiar el código de error.
Esperar a que se cargue todo el contenido para rastreadores y generación estática
Streamear ofrece una mejor experiencia de usuario porque permite ver el contenido a medida que este está disponible.
Sin embargo, cuando un rastreador visita tu página, o si generas las páginas en tiempo de compilacón, quizá prefieras dejar que todo el contenido cargue primero, y después producir el HTML final en lugar de revelarlo de forma progresiva.
Puedes esperar a que todo el contenido cargue esperando a la promesa stream.allReady
:
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 = // ... depende de tu estrategia de detección de bots ...
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' },
});
}
}
Un visitante regular verá cargar el contenido de forma progresiva. Un rastreador recibirá el HTML final cuando toda la información haya cargado. No obstante, esto también significa que el rastreador tendrá que esperar a que toda la información cargue, lo cual puede fallar o tardar demasiado. Dependiendo de tu app, también puedes considerar si enviar todo el shell a los rastreadores.
Abortar el renderizado en el servidor
Puedes forzar al servidor a abandonar el renderizado después de un tiempo de espera:
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 eliminará los respaldos de carga restantes como HTML e intentará renderizar el resto en el cliente.