react-helmet-async
react-helmet-async copied to clipboard
Confused: Seems to work fine even when head is output as part of `rendeToNodeStream`
Hi!
I just converted my SSR app to use renderToNodeStream and according to some of my Apache Bench benchmarks, it seems to be working (all requests are completed in comparison with renderToString which would block and cause the app to eventually crash).
The section about renderToNodeString in the README says that it can only be used with this lib if the head is output outisde renderToNodeString. However, in my case the <head> is output by renderToNodeString and everything seems to be working fine.
Now, maybe something is wrong and I haven't noticed, but I have tested changing the head from sub-components (the title) and it worked well. However, I'm afraid I'm breaking something I don't fully understand.
SSR in my app is implemented by a middleware called serverRenderer, here are the relevant contents:
import * as React from 'react';
import * as express from 'express';
import { renderToNodeStream } from 'react-dom/server';
import { StaticRouter as Router, matchPath } from 'react-router-dom';
import App from '../../shared/App';
import Html from '../components/HTML';
import { ServerStyleSheets, ThemeProvider } from '@material-ui/styles';
import theme from '../../shared/mui-theme';
import { HelmetProvider } from 'react-helmet-async';
import { getSnapshot } from 'mobx-state-tree';
import { getDataFromTree } from 'mst-gql';
import { RootStoreType, StoreContext } from '../../shared/models';
import { initializeStore } from '../../shared/utils/initModels';
import IntlProvider from '../../shared/i18n/IntlProvider';
const routerContext = {};
const helmetContext = {};
const serverRenderer: any = () => async (req: express.Request, res: express.Response, next: any) => {
const match = ssrRoutes.find(route => matchPath(req.path, {
path: route,
exact: true
}));
const store = initializeStore(true, undefined, req.header('Cookie'));
const comp =
<StoreContext.Provider value={store}>
<Router location={req.url} context={routerContext}>
<IntlProvider>
<HelmetProvider context={helmetContext}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</HelmetProvider>
</IntlProvider>
</Router>
</StoreContext.Provider>;
const sheets = new ServerStyleSheets();
const content = await getDataFromTree(sheets.collect(comp), store);
const initialState = getSnapshot<RootStoreType>(store);
const css = sheets.toString();
res.write('<!doctype html>');
const html =
<Html
css={[res.locals.assetPath('bundle.css'), res.locals.assetPath('vendor.css')]}
helmetContext={helmetContext}
scripts={[res.locals.assetPath('bundle.js'), res.locals.assetPath('vendor.js')]}
inlineStyle={css}
state={JSON.stringify(initialState)}
>
{content}
</Html>;
renderToNodeStream(html).pipe(res);
};
export default serverRenderer;
The HTML helper is the one that has the actual HTML structure, here's how it looks like
import React from 'react';
import Helmet from 'react-helmet';
interface IProps {
children: any;
css: string[];
helmetContext: any;
inlineStyle: string;
scripts: string[];
state: string;
}
const HTML = ({
children,
inlineStyle = '',
css = [],
helmetContext: { helmet },
scripts = [],
state = '{}'
}: IProps) => {
const head = Helmet.renderStatic();
return (
<html lang=''>
<head>
<meta charSet='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
{helmet.base.toComponent()}
{helmet.title.toComponent()}
{helmet.meta.toComponent()}
{helmet.link.toComponent()}
{helmet.script.toComponent()}
{css.filter(Boolean).map(href => (
<link key={href} rel='stylesheet' href={href} />
))}
<style id='jss-server-side' dangerouslySetInnerHTML={{__html: inlineStyle}}></style>
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
// TODO: Add jsesc/stringify here
// see: https://twitter.com/HenrikJoreteg/status/1143953338284703744
__html: `window.__PRELOADED_STATE__ = ${state}`,
}}
/>
</head>
<body>
{/* eslint-disable-next-line react/no-danger */}
<div id='app' dangerouslySetInnerHTML={{ __html: children }} />
{scripts.filter(Boolean).map(src => (
<script key={src} src={src} />
))}
</body>
</html>
);
};
export default HTML;
And it (seems) to work well, requests are properly rendered server-side, head attributes are changed when using <Helmet> from components.
I'd really love some insights on why it works well if it's recommended against, perhaps you could elaborate a bit more about this?
Thanks!
EDIT: I see I'm using Helmet.renderStatic in the HTML render helper. Maybe that could be an issue, although it seems to work. Am I even supposed to use methods from regular Helmet like this (I left it there by accident). If not, what would be the proper way to do it considering the code above?
Thanks in advance!
Any insights?
import React from 'react';
import Helmet from 'react-helmet'; // you are still using react-helmet, not react-helmet-async
interface IProps {
children: any;
css: string[];
helmetContext: any;
inlineStyle: string;
scripts: string[];
state: string;
}