emotion
emotion copied to clipboard
How to use emotion with renderToPipeableStream
I'm looking at how to implement SSR emotion with react 18. React18 made renderToPipeableStream method the new recommended way and deprecated the renderToNodeStream method.
The documentation only seems to deal with the now deprecated renderToNodeStream and I can't figure out how to make it works with the pipeable stream.
cf: https://reactjs.org/docs/react-dom-server.html#rendertonodestream https://emotion.sh/docs/ssr#renderstylestonodestream
Documentation links: https://emotion.sh/docs/ssr
We didn't implement integration with renderToPipeableStream yet.
Hey there :) Will this feature come in near future ?
Is there any roadmap/plan when this is going to be integrated?
Hello looking forward to this feature, how can we help make its implementation?
You could try implementing PoC for this. We could hop on a call to discuss some challenges and stuff before that - if you would be interested in this sort of a thing.
kind of hacky but it works for the server side.
const stylesPipeableStream =
(res: Response, cache: EmotionCache, nonceString: string) => {
let content = '';
const inlineStream = new Writable({
write(chunk, _encoding, cb) {
let match;
content = content.concat(chunk.toString());
let regex = new RegExp(`<style data-emotion="${cache.key} ([a-zA-Z0-9-_ ]+)">(.+)<\\/style>`, 'gm');
regex.lastIndex = 0;
while ((match = regex.exec(content)) !== null) {
const id = match[1];
const css = match[2];
cache.nonce = nonceString;
cache.inserted[id] = css;
}
res.write(chunk, cb);
},
final() {
res.end();
},
});
return inlineStream;
};
onShellReady() {
...
stream.pipe(stylesPipeableStream(res, cache, nonce));
},
bootstrapScriptContent: `
${Object.keys(cache.inserted).map(id => `
if(document.querySelectorAll('[data-emotion="${cache.key} ${id}"]').length === 0) {
document.head.innerHTML += '<style data-emotion="${cache.key} ${id}"${!cache.nonce ? '' : ` nonce="${cache.nonce}"`}>${cache.inserted[id].toString()}</style>';
}`).join('\n')}
...
`
Note that this won't work correctly with Suspense because you are only awaiting the shell. A more complete solution would append <script/> to our <style/>s that would move the preceding <style> to document.head. On top of that, you'd have to figure out how to properly rehydrate all the streamed <style/>s.
yeah and to figure out how to avoid the hydratation missmatch that force react to re render. https://github.com/facebook/react/issues/24430
This solution is far from ideal that is why I didn't open a pull request with this code but at least it give some short term fix in my case to the unstyled page flashing on loading
Hm, the quoted issue is somewhat weird - I don't get why React would be bothered by a script injected before <head/>. Hydration should only occur within the root (as far as I know) and that's usually part of the <body/>. Note thought that I didn't read the whole thread there.
This solution is far from ideal that is why I didn't open a pull request with this code but at least it give some short term fix in my case to the unstyled page flashing on loading
I think that you could try to insert the appropriate <script/> within write into the chunk before calling res.write with it. The simplest approach, for now, would be to insert:
<script>
var s = document.currentScript;
document.head.appendChild(s.previousSibling);
s.remove();
</script>
A lot of metaframeworks like next.js and remix render the whole document afaik. Would it be possible to create a suspense component that renders a style tag with all the aggregated styles that resolves after the rest of the document has resolved. That might keep react happy?
Remix released CSS-in-JS support with v1.11.0 does that help here?
@Andarist to make sure I understand correctly, does the work you're doing on styled-components carry over here to emotion?
https://github.com/styled-components/styled-components/issues/3658
@Andarist can you tell us where we stand here?
- Is Emotion just not going to be able to support SSR (renderToPipeableStream) anytime soon?
- Are there any solid work around? I saw in the linked 'styled-components' you have a potential one using a custom Writable, any updates there and/or an example usage would be awesome
Sorry to be a bother, this just holds up our usage of MUI and keeps us on React v17
For Remix users, you can follow this example. The example is with Chakra but I believe you can use the same approach for any Emotion js setup
That solution basically readers the entire app and defeats the purpose of streaming.
Im working on a POC to address this with Andraist code he provided on the Linked Styled-Components issue. However, he posted that it wasnt his latest version and would try to find it but never posted a follow-up there
Once i have my POC in a good spot ill post it here
I just found a small work-around for this situation.
const {pipe, abort} = renderToPipeableStream(
<CacheProvider value={cache}>
<YourAPP />
</CacheProvider>,
{
onShellReady() {
const body = new PassThrough();
let oldContent = '';
body.on('data', (chunk) => {
const chunkStr = chunk.toString()
if (chunkStr.endsWith('<!-- emotion ssr markup -->')) {
return;
}
if (chunkStr.endsWith('</html>') || chunkStr.endsWith('</script>')) {
const keys = Object.keys(cache.inserted);
const content = keys.map(key => cache.inserted[key]).join(' ');
if (content === oldContent) {
return;
}
body.write(`<style data-emotion-streaming>${content}</style><!-- emotion ssr markup -->`);
oldContent = content;
}
});
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
}
);
After each chunk is spit out, a NEW STYLE TAG is appended to the html according to the contents of the emotion cache.
(A little trick is used here - I determine the end of each valid chunk of react 18 just by simple string matching.)
From my testing, this guarantees that the styles are correct when Server Side Rendering.
@Andarist
. Hydration should only occur within the rootHm, the quoted issue is somewhat weird - I don't get why React would be bothered by a script injected before
- Astro can control when React components are hydrated. https://docs.astro.build/en/core-concepts/framework-components/
- You can hydrate them for the first time when they are in the view area.
- Hydration roots of components in Astro is not the document root but the component roots. https://docs.astro.build/en/concepts/islands/
I stopped using Emotion due to this and lack of support for RSC. I'll recommend everyone to stop using it and migrate to SCSS Modules or vanilla-extract (or somethig better zero-runtime solutions).
Over 1 year later and emotion still hasn't figure this out? I waited a very long time to upgrade to React 18 and I'm still stuck with rendering the whole document because of this one issue. And I'm only using styled-components with @mui.