We are trying to migrate to react 18. Our current code uses React 17 with @loadable for chunking at server and client with renderToString. On simple migration to React 18 gives hydration error even on rendering just a div.
On researching, I found these two things,
- React 18 introduced streaming SSR, and renderToString is a blocking operation. This means it won't capture loadable components that haven't been loaded yet.
- @loadable issue --> https://github.com/gregberge/loadable-components/issues/718
We would love to migrate to renderToPipeableStream but it will take sometime but if anyone can help with migrating to renderToPipepableStream with/without loadable or making renderToString work, that would be great. For reference Attaching code snippet for sever and client side below
SERVER
let appContent = '';
let headCss = '';
const statsFile = path.resolve(cwd, clientOutput, 'loadable-stats.json');
const extractor = new ChunkExtractor({statsFile, entrypoints: [applicationName]});
try {
const sheet = new ServerStyleSheet();
const jsx = extractor.collectChunks(
<div id="ssr_random_2">Hello World</div>;,
);
appContent = renderToString(sheet.collectStyles(jsx));
console.log('After renderToString);
styleCss = sheet.getStyleTags(); // or sheet.getStyleElement();
} catch (err) {
console.log('err...', err);
shouldCache = false;
console.log('renderToStringFailed)
}
const helmet = Helmet.renderStatic();
const helmetObj = {
htmlAttributes: helmet.htmlAttributes.toString(),
title: helmet.title.toString(),
meta: helmet.meta.toString(),
link: helmet.link.toString(),
};
const buildScriptTags = arr =>
arr
.map(({props}) => {
if (
props.id === '__LOADABLE_REQUIRED_CHUNKS__' ||
props.id === '__LOADABLE_REQUIRED_CHUNKS___ext'
) {
return `<script id=${props.id} type="application/json">${props.dangerouslySetInnerHTML.__html}</script>`;
} else {
return `<script ${Object.entries(props)
.map(([key, val]) => {
return key === 'async' ? 'defer' : `${key}="${val}"`;
})
.join(' ')}></script>`;
}
})
.join('\n');
const scriptTags = buildScriptTags(extractor.getScriptElements());
const now = new Date();
const cachedAt = now.toLocaleString();
const data = {
appContent,
styleCss,
storeJson: store.getState(),
helmetObj,
cachedAt,
responseStatus,
scriptTags,
};
response.status(responseStatus);
let htmlTemplate = serverResponseTemplate(
{...data, assets, publicPath}
);
console.log('After htmlTemplate initialization');
response.send(htmlTemplate);
ServerResponseTemplate
export default function serverResponseTemplate(
{
appContent = '',
styleCss = '',
storeJson = {},
helmetObj,
cachedAt,
assets,
publicPath,
scriptTags,
}) {
return `
<!DOCTYPE html>
<html lang="en" ${helmetObj.htmlAttributes}>
<head>
<script>
var starttime = new Date();
</script>
${helmetObj.link || ''}
${styleCss}
${assets
.map(asset => {
let assetName = asset && asset.name ? asset.name : asset;
if (assetName.endsWith('.css')) {
return `<link href="${publicPath}${assetName}" rel="stylesheet" type="text/css">`;
}
return '';
})
.join('')}
${scriptTags}
</head>
<body>
<div id="root">${appContent}</div>
<script charSet="UTF-8">
window.cachedAt="${cachedAt}";
</script>
</body>
</html>
`;
}
Client
const domNode = document.getElementById('root');
loadableReady(() => {
hydrateRoot(domNode, <div id="ssr_random_2">Hello World</div>, {
onRecoverableError,
});
});