renderToPipeableStream
renderToPipeableStream
renderiza uma árvore React em um Node.js Stream. que pode ser canalizada.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
- Referência
- Uso
- Renderizando uma árvore React como HTML para um Node.js Stream
- Streaming mais conteúdo à medida que ele carrega
- Especificando o que entra no shell
- Registrando falhas no servidor
- Recuperando de erros dentro do shell
- Recuperando de erros fora do shell
- Definindo o código de status
- Lidando com diferentes erros de maneiras diferentes
- Esperando o carregamento de todo o conteúdo para crawlers e geração estática
- Interrompendo a renderização do servidor
Referência
renderToPipeableStream(reactNode, options?)
Chame renderToPipeableStream
para renderizar sua árvore React como HTML em um 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);
}
});
No cliente, chame hydrateRoot
para tornar o HTML gerado no servidor interativo.
Parâmetros
-
reactNode
: Um nó React que você deseja renderizar em HTML. Por exemplo, um elemento JSX como<App />
. Espera-se que ele represente o documento inteiro, então o componenteApp
deve renderizar a tag<html>
. -
opcional
options
: Um objeto com opções de streaming.- opcional
bootstrapScriptContent
: Se especificado, esta string será colocada em uma tag<script>
inline. - opcional
bootstrapScripts
: Uma matriz de URLs de string para as tags<script>
a serem emitidas na página. Use isso para incluir o<script>
que chamahydrateRoot
. Omita-o se você não quiser executar o React no cliente. - opcional
bootstrapModules
: ComobootstrapScripts
, mas emite<script type="module">
em vez disso. - opcional
identifierPrefix
: Um prefixo de string que o React usa para IDs gerados poruseId
. Útil para evitar conflitos ao usar vários roots na mesma página. Deve ser o mesmo prefixo passado parahydrateRoot
. - opcional
namespaceURI
: Uma string com o namespace URI do root para o stream. O padrão é HTML comum. Passe'http://www.w3.org/2000/svg'
para SVG ou'http://www.w3.org/1998/Math/MathML'
para MathML. - opcional
nonce
: Uma stringnonce
para permitir scripts parascript-src
Content-Security-Policy. - opcional
onAllReady
: Um callback que é acionado quando toda a renderização estiver concluída, incluindo o shell e todo o conteúdo adicional. Você pode usá-lo em vez deonShellReady
para crawlers e geração estática. Se você iniciar o streaming aqui, não receberá nenhum carregamento progressivo. O stream conterá o HTML final. - opcional
onError
: Um callback que é acionado sempre que houver um erro do servidor, seja recuperável ou não. Por padrão, isso só chamaconsole.error
. Se você substituí-lo para registrar relatórios de falha, certifique-se de ainda chamarconsole.error
. Você também pode usá-lo para ajustar o código de status antes que o shell seja emitido. - opcional
onShellReady
: Um callback que é acionado logo após o shell inicial ser renderizado. Você pode definir o código de status e chamarpipe
aqui para iniciar o streaming. O React fará o streaming do conteúdo adicional após o shell, juntamente com as tags<script>
inline que substituem os fallbacks de carregamento HTML com o conteúdo. - opcional
onShellError
: Um callback que é acionado se houver um erro ao renderizar o shell inicial. Ele recebe o erro como um argumento. Nenhum byte foi emitido do stream ainda, e nemonShellReady
nemonAllReady
serão chamados, para que você possa gerar um fallback shell HTML. - opcional
progressiveChunkSize
: O número de bytes em um chunk. Leia mais sobre a heurística padrão.
- opcional
Retorna
renderToPipeableStream
retorna um objeto com dois métodos:
pipe
gera o HTML no Node.js Stream gravável fornecido. Chamepipe
emonShellReady
se você deseja ativar o streaming ou emonAllReady
para crawlers e geração estática.abort
permite que você interrompa a renderização do servidor e renderize o restante no cliente.
Uso
Renderizando uma árvore React como HTML para um Node.js Stream
Chame renderToPipeableStream
para renderizar sua árvore React como HTML em um Node.js Stream:
import { renderToPipeableStream } from 'react-dom/server';
// A sintaxe do manipulador de rota depende do seu framework de backend
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
Junto com o componente root, você precisa fornecer uma lista de caminhos de <script>
bootstrap. Seu componente root deve retornar o documento inteiro, incluindo a tag root <html>
.
Por exemplo, pode se parecer com isto:
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>
);
}
O React irá injetar o doctype e suas tags <script>
de bootstrap no stream HTML resultante:
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>
No cliente, seu script bootstrap deve hidratar o document
inteiro com uma chamada para hydrateRoot
:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
Isso irá anexar event listeners ao HTML gerado pelo servidor e torná-lo interativo.
Deep Dive
As URLs dos assets finais (como arquivos JavaScript e CSS) são frequentemente hasheadas após a build. Por exemplo, em vez de styles.css
, você pode acabar com styles.123456.css
. Hashear os nomes de arquivos de assets estáticos garante que cada build distinto do mesmo asset terá um nome de arquivo diferente. Isso é útil porque permite que você habilite o cache de longo prazo com segurança para assets estáticos: um arquivo com um determinado nome nunca alteraria o conteúdo.
No entanto, se você não souber as URLs dos assets até depois da build, não há como colocá-las no código-fonte. Por exemplo, codificar "/styles.css"
no JSX, como antes, não funcionaria. Para mantê-los fora do seu código-fonte, seu componente root pode ler os nomes de arquivos reais de um mapa passado como prop:
export default function App({ assetMap }) {
return (
<html>
<head>
...
<link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}
No servidor, renderize <App assetMap={assetMap} />
e passe seu assetMap
com as URLs dos assets:
// Você precisaria obter este JSON de suas ferramentas de build, por ex. lê-lo da saída da 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);
}
});
});
Como seu servidor agora está renderizando <App assetMap={assetMap} />
, você precisa renderizá-lo com assetMap
no cliente também para evitar erros de hidratação. Você pode serializar e passar assetMap
para o cliente assim:
// Você precisaria obter este JSON de suas ferramentas de build.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// Cuidado: É seguro stringificar() isto porque estes dados não são gerados pelo usuário.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
No exemplo acima, a opção bootstrapScriptContent
adiciona uma tag <script>
inline extra que define a variável global window.assetMap
no cliente. Isso permite que o código do cliente leia o mesmo assetMap
:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
Tanto o cliente quanto o servidor renderizam App
com a mesma prop assetMap
, então não há erros de hidratação.
Streaming mais conteúdo à medida que ele carrega
O streaming permite que o usuário comece a ver o conteúdo mesmo antes que todos os dados tenham sido carregados no servidor. Por exemplo, considere uma página de perfil que mostra uma capa, uma barra lateral com amigos e fotos e uma lista de posts:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}
Imagine que carregar dados para <Posts />
leva algum tempo. Idealmente, você gostaria de mostrar o restante do conteúdo da página de perfil para o usuário sem esperar pelos posts. Para fazer isso, encapsule Posts
em uma boundary <Suspense>
:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Isso informa ao React para começar a transmitir o HTML antes que Posts
carregue seus dados. O React enviará primeiro o HTML para o fallback de carregamento (PostsGlimmer
) e, em seguida, quando Posts
terminar de carregar seus dados, o React enviará o HTML restante junto com uma tag <script>
inline que substitui o fallback de carregamento por esse HTML. Da perspectiva do usuário, a página aparecerá primeiro com o PostsGlimmer
, posteriormente substituído pelos Posts
.
Você pode ainda anexar boundaries <Suspense>
para criar uma sequência de carregamento mais granular:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
Neste exemplo, o React pode começar a transmitir a página ainda mais cedo. Apenas ProfileLayout
e ProfileCover
devem terminar a renderização primeiro porque não estão envolvidos em nenhuma boundary <Suspense>
. No entanto, se Sidebar
, Friends
ou Photos
precisarem carregar alguns dados, o React enviará o HTML para o fallback BigSpinner
em vez disso. Em seguida, à medida que mais dados se tornam disponíveis, mais conteúdo continuará a ser revelado até que tudo se torne visível.
O streaming não precisa esperar que o próprio React carregue no navegador ou que seu aplicativo se torne interativo. O conteúdo HTML do servidor será revelado progressivamente antes que qualquer uma das tags <script>
carregue.
Leia mais sobre como o streaming HTML funciona.
Especificando o que entra no shell
A parte do seu aplicativo fora de qualquer boundary <Suspense>
é chamada de shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
Ele determina o estado de carregamento mais antigo que o usuário pode ver:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>
Se você envolver todo o aplicativo em uma boundary <Suspense>
na raiz, o shell conterá apenas esse spinner. No entanto, essa não é uma boa experiência do usuário, porque ver um spinner grande na tela pode parecer mais lento e mais irritante do que esperar um pouco mais e ver o layout real. É por isso que, geralmente, você vai querer colocar as boundaries <Suspense>
para que o shell pareça mínimo, mas completo - como um esqueleto de todo o layout da página.
O callback onShellReady
é acionado quando todo o shell foi renderizado. Normalmente, você iniciará o streaming então:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
No momento em que onShellReady
é ativado, os componentes nas boundaries <Suspense>
aninhadas ainda podem estar carregando dados.
Registrando falhas no servidor
Por padrão, todos os erros no servidor são registrados no console. Você pode substituir esse comportamento para registrar relatórios de falha:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
Se você fornecer uma implementação onError
personalizada, não se esqueça de também registrar erros no console como acima.
Recuperando de erros dentro do shell
Neste exemplo, o shell contém ProfileLayout
, ProfileCover
e PostsGlimmer
:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Se ocorrer um erro ao renderizar esses componentes, o React não terá nenhum HTML significativo para enviar ao cliente. Substitua onShellError
para enviar um HTML de fallback que não dependa da renderização do servidor como último recurso:
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);
}
});
Se houver um erro ao gerar o shell, onError
e onShellError
serão disparados. Use onError
para relatórios de erros e use onShellError
para enviar o documento HTML de fallback. Seu HTML de fallback não precisa ser uma página de erro. Em vez disso, você pode incluir um shell alternativo que renderize seu aplicativo apenas no cliente.
Recuperando de erros fora do shell
Neste exemplo, o componente <Posts />
está envolvido em <Suspense>
para que não faça parte do shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Se ocorrer um erro no componente Posts
ou em algum lugar dentro dele, o React tentará se recuperar dele:
- Ele emitirá o fallback de carregamento para o
<Suspense>
mais próximo (PostsGlimmer
) no HTML. - Ele “desistirá” de tentar renderizar o conteúdo
Posts
no servidor. - Quando o código JavaScript carrega no cliente, o React tentará renderizar
Posts
no cliente.
Se tentar renderizar Posts
no cliente também falhar, o React lançará o erro no cliente. Como acontece com todos os erros lançados durante a renderização, a error boundary pai mais próxima determina como apresentar o erro ao usuário. Na prática, isso significa que o usuário verá um indicador de carregamento até que seja certo que o erro não é recuperável.
Se tentar renderizar Posts
no cliente for bem-sucedido, o fallback de carregamento do servidor será substituído pela saída de renderização do cliente. O usuário não saberá que houve um erro do servidor. No entanto, o callback onError
do servidor e os callbacks onRecoverableError
do cliente serão disparados para que você possa ser notificado sobre o erro.
Definindo o código de status
O streaming introduz uma troca. Você deseja começar a transmitir a página o mais cedo possível para que o usuário possa ver o conteúdo mais cedo. No entanto, assim que você começar a transmitir, você não poderá mais definir o código de status da resposta.
Ao dividir seu aplicativo no shell (acima de todas as boundaries <Suspense>
) e o restante do conteúdo, você já resolveu parte desse problema. Se o shell emitir um erro, você receberá o callback onShellError
que permite definir o código de status do erro. Caso contrário, você sabe que o aplicativo pode se recuperar no cliente, então você pode enviar “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);
}
});
Se um componente fora do shell (ou seja, dentro de uma boundary <Suspense>
) lançar um erro, o React não parará de renderizar. Isso significa que o callback onError
será acionado, mas você ainda receberá onShellReady
em vez de onShellError
. Isso ocorre porque o React tentará se recuperar desse erro no cliente, como descrito acima.
No entanto, se você quiser, pode usar o fato de algo ter dado errado para definir o código de 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>Something went wrong</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
Isso só detectará erros fora do shell que ocorreram ao gerar o conteúdo inicial do shell, portanto, não é exaustivo. Se saber se um erro ocorreu para algum conteúdo for fundamental, você poderá movê-lo para o shell.
Lidando com diferentes erros de maneiras diferentes
Você pode criar suas próprias subclasses Error
e usar o operador instanceof
para verificar qual erro é lançado. Por exemplo, você pode definir um NotFoundError
personalizado e lançá-lo do seu componente. Em seguida, seus callbacks onError
, onShellReady
e onShellError
podem fazer algo diferente dependendo do tipo de erro:
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);
}
});
Lembre-se que, uma vez que você emite o shell e inicia o streaming, você não pode alterar o código de status.
Esperando o carregamento de todo o conteúdo para crawlers e geração estática
O streaming oferece uma melhor experiência do usuário porque o usuário pode ver o conteúdo à medida que ele fica disponível.
No entanto, quando um crawler visita sua página ou se você estiver gerando as páginas no tempo de build, talvez você queira que todo o conteúdo seja carregado primeiro e, em seguida, produza a saída HTML final em vez de revelá-lo progressivamente.
Você pode aguardar o carregamento de todo o conteúdo usando o callback onAllReady
:
let didError = false;
let isCrawler = // ... depende da sua estratégia de detecção de bot ...
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);
}
});
Um visitante normal receberá um fluxo de conteúdo carregado progressivamente. Um crawler receberá a saída HTML final após o carregamento de todos os dados. No entanto, isso também significa que o crawler terá que esperar por todos os dados, alguns dos quais podem ser lentos para carregar ou dar erro. Dependendo do seu app, você pode optar por enviar o shell para os crawlers também.
Interrompendo a renderização do servidor
Você pode forçar a “desistência” da renderização do servidor após um tempo limite:
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);
O React irá descarregar os fallbacks de carregamento restantes como HTML e tentará renderizar o restante no cliente.