emotion-js / emotion

👩‍🎤 CSS-in-JS library designed for high performance style composition
https://emotion.sh/
MIT License
17.4k stars 1.11k forks source link

How to use emotion with renderToPipeableStream #2800

Open 0Lucifer0 opened 2 years ago

0Lucifer0 commented 2 years ago

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

Andarist commented 2 years ago

We didn't implement integration with renderToPipeableStream yet.

Tjerk-Haaye-Henricus commented 2 years ago

Hey there :) Will this feature come in near future ?

skriems commented 2 years ago

Is there any roadmap/plan when this is going to be integrated?

willopez commented 2 years ago

Hello looking forward to this feature, how can we help make its implementation?

Andarist commented 2 years ago

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.

0Lucifer0 commented 2 years ago

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')}
...
`
Andarist commented 2 years ago

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.

0Lucifer0 commented 2 years ago

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

Andarist commented 2 years ago

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>
waspeer commented 2 years ago

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?

sgabriel commented 1 year ago

Remix released CSS-in-JS support with v1.11.0 does that help here?

indeediansbrett commented 1 year ago

@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

LorenDorez commented 1 year ago

@Andarist can you tell us where we stand here?

  1. Is Emotion just not going to be able to support SSR (renderToPipeableStream) anytime soon?
  2. 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

WesleyYue commented 1 year ago

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

LorenDorez commented 1 year ago

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

St2r commented 1 year ago

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.

tats-u commented 12 months ago

@Andarist

Hm, the quoted issue is somewhat weird - I don't get why React would be bothered by a script injected before . Hydration should only occur within the root

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).

heath-freenome commented 10 months ago

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.

piotrblasiak commented 1 month ago

Another year has gone. Are there any plans to support this?

karol-f commented 1 month ago

Another year has gone. Are there any plans to support this?

At this time, everyone is moving to solutions that works fine with React Server Components and Next.js App Router

piotrblasiak commented 1 month ago

Another year has gone. Are there any plans to support this?

At this time, everyone is moving to solutions that works fine with React Server Components and Next.js App Router

You mean other libraries? Like?

karol-f commented 1 month ago

Multiple solutions like zero-runtime CSS-in-JS libraries

https://mui.com/blog/introducing-pigment-css/ , https://github.com/mui/pigment-css?tab=readme-ov-file#how-to-guides

In the era of React Server Components and the Next.js App Router, component libraries like Material UI must make some paradigm-shifting changes to reap the potential performance gains by moving more of the work of rendering UIs from client to server.

Trouble is, the "traditional" CSS-in-JS solutions we rely on aren't able to come along with us because the React context API only works on the client.

or e.g.

Panda.css - CSS-in-JS can make it easy to build dynamic UI components, it usually comes with some pretty expensive performance tradeoffs. And since most CSS-in-JS libraries don’t work reliably with server components, it’s clear that the whole paradigm needs to evolve, or risk becoming irrelevant. That’s where Panda comes in. It’s a new styling engine that aims to combine the DX of CSS-in-JS with the performance of atomic CSS, in a way that’s both type-safe and RSC compatible.

Or even CSS Modules or Tailwind.

Also check Next.js docs - https://nextjs.org/docs/app/building-your-application/styling/css-in-js

The following libraries are supported in Client Components in the app directory (alphabetical)

rockyshi1993 commented 1 month ago

I wrote a framework that uses renderToPipeableStream to render pages. It only needs to add the following two lines of code in the entry file, but I am not sure this method is suitable for all frameworks. import createCache from '@emotion/cache'; createCache({ key:'css' });

You can check how to configure it here: https://github.com/rockyshi1993/nodestack?tab=readme-ov-file#how-to-perform-server-side-rendering-ssr

rockyshi1993 commented 1 month ago

又一年过去了。有什么计划支持这一点吗?

React Server Componen You can see if this framework is suitable for you, it uses renderToPipeableStream to render pages https://github.com/rockyshi1993/nodestack?tab=readme-ov-file#how-to-perform-server-side-rendering-ssr