I'm trying to do SSR with ReactDOMServer.renderToNodeStream(element)
but just wanted to know if there would be any problem with using both ReactDOMServer.renderToString(element)
and ReactDOMServer.renderToNodeStream(element)
at each request?
What I have in my custom SSR setup is: * React 16 * react-loadable * styled-components v4 * react-helmet-async * Redux * Express JS
Previously with React, I could easily render a HTML document by first rendering the <head></head>
tags that contains markup produced by react-helmet
and then using ReactDOMServer.renderToString()
to render my React elements.
However, by switching to ReactDOMServer.renderToNodeStream()
I had to switch react-helmet
for react-helmet-async
, which supports renderToNodeStream()
function. But then when I try to render the <head></head>
tags with the markup by react-helmet-async
it'll come back as undefined
.
To get around this problem, I've had to use renderToString()
first without actually writing that out to Express JS response
. That way react-helmet-async
can then see what meta tags to render and then proceed to use renderToNodeStream
and stream that out to the response
.
I've simplified my code as much as possible as I want to understand if this would have a negative impact (for performance, or if anyone can think of anything else)?
Before:
let html = ReactDOMServer.renderToString(stylesheet.collectStyles(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<LocalStoreProvider store={store}>
<HelmetProvider context={helmetContext}>
<RouterContext {...renderProps} />
</HelmetProvider>
</LocalStoreProvider>
</Loadable.Capture>
));
const { helmet } = helmetContext;
response.write(
renderDocumentHead({
css: stylesheet.getStyleTags(),
title: helmet.title.toString(),
link: helmet.link.toString(),
meta: helmet.meta.toString()
})
);
response.write(html);
After:
let html = stylesheet.collectStyles(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<LocalStoreProvider store={store}>
<HelmetProvider context={helmetContext}>
<RouterContext {...renderProps} />
</HelmetProvider>
</LocalStoreProvider>
</Loadable.Capture>
);
// do a first pass render so that react-helmet-async
// can see what meta tags to render
ReactDOMServer.renderToString(html);
const { helmet } = helmetContext;
response.write(
renderDocumentHead({
css: stylesheet.getStyleTags(),
title: helmet.title.toString(),
link: helmet.link.toString(),
meta: helmet.meta.toString()
})
);
const stream = stylesheet.interleaveWithNodeStream(
ReactDOMServer.renderToNodeStream(html)
);
// and then actually stream the react elements out
stream.pipe(response, { end: false });
stream.on('end', () => response.end('</body></html>'));
Unfortunately, the only way I could get react-helmet-async
to work correctly, I have to do this two-pass render. My CSS styles, etc. resolves correctly and the client renders/hydrates correctly too. I've seen other examples where react-apollo
was used and the getDataFromTree
data rehydration method was used which allows react-helmet-async
to see what was needed to render the head markup. But hopefully there are no issues with my two-pass rendering approach?