emotion icon indicating copy to clipboard operation
emotion copied to clipboard

Is it possible to create a emotion cache in the Server mode on browser?

Open Jack-Works opened this issue 3 years ago • 53 comments

Current behavior: Use emotion cache with @emotion/server cannot extract CSS on the browser.

To reproduce:

https://codesandbox.io/s/emotion-issue-template-forked-ij0lf7?file=/src/main.jsx

Expected behavior: I want to run SSR in browser

Environment information:

  • react version: 17
  • @emotion/react version: 11

Jack-Works avatar Mar 21 '22 05:03 Jack-Works

Did you perhaps forget to save your codesandbox? It's not using @emotion/server anywhere as far as I can tell.

Andarist avatar Mar 21 '22 14:03 Andarist

Oh sorry, here is the new one. and I don't know why it does not work even I'm not using @emotion/server

https://codesandbox.io/s/emotion-issue-template-forked-1j1g0n?file=/src/main.jsx

the code sandbox link in the issue template is too old that use react-scripts v2 (latest is v5) and emotion 10. I have to manually upgrade them and everything breaks 😂

Jack-Works avatar Mar 21 '22 14:03 Jack-Works

Thanks for pointing that out @Jack-Works, I can fix the CodeSandbox soon. Created https://github.com/emotion-js/emotion/issues/2692.

srmagura avatar Mar 21 '22 15:03 srmagura

@Jack-Works CacheProvider is using value prop (just like all Providers created using React.createContext) and not cache prop to accept a cache. When I fix this I get this output:

Hello world!!

Is this matching your expectations? Or is this missing something?

Andarist avatar Mar 21 '22 16:03 Andarist

Hello world!!

As you can see, my style is not collected on the style sheet. the style tag above is empty.

css`
    color: red;
    font-size: 48px;
`

Jack-Works avatar Mar 22 '22 02:03 Jack-Works

This is an interesting use case that might be worth supporting. While this is an interesting use case - it's also rather a niche use case. For that reason, I would prefer to figure out a solution that wouldn't make us add too much code to the "common path" of Emotion.

I think the answer to that is to have a different code for @emotion/server when it is loaded in a browser. I've hacked around a few lines of code patching cache.insert to make it work without any changes elsewhere: https://codesandbox.io/s/emotion-issue-template-forked-2qetz0?file=/src/main.jsx

However, there is one caveat here for now... this doesn't work for global styles, only the scoped styles are handled. I'm not yet sure how to make global styles work but perhaps this is just a matter of some more thought.

There is also a question here though - do you really want to run this in a DOM environment? or just in a web worker or smth like this? Cause if the latter then we are working on introducing alternative builds for those envs and with those builds @emotion/server should work "out of the box" for the most part. The progress for this can be tracked here: https://github.com/preconstruct/preconstruct/pull/435

Andarist avatar Mar 22 '22 08:03 Andarist

Maybe createCache can receive the isBrowser from the options?

There is also a question here though - do you really want to run this in a DOM environment? or just in a web worker or smth like this?

Actually, I'm still thinking about it. We do have a use case to run SSR in the browser, but maybe I can move it into a Web Worker.

I think the answer to that is to have a different code for @emotion/server when it is loaded in a browser. I've hacked around a few lines of code patching cache.insert to make it work without any changes elsewhere: https://codesandbox.io/s/emotion-issue-template-forked-2qetz0?file=/src/main.jsx

Thanks for your hack! I'll try if that works for us! If it works well (or mostly well), I'm fine to use hacks to make it work.

Jack-Works avatar Mar 22 '22 11:03 Jack-Works

Maybe createCache can receive the isBrowser from the options?

It could but then we'd have to include all the related code in the "common path" and I'd like to avoid this.

Actually, I'm still thinking about it. We do have a use case to run SSR in the browser, but maybe I can move it into a Web Worker.

I still think there is merit in supporting this in a DOM environment - I've just mentioned that the web worker support might come sooner. The only blocker for the DOM environment right now is to figure out how to handle the global styles. I think that the rest should be fairly easy to implement within @emotion/server (using export conditions etc).

Thanks for your hack! I'll try if that works for us! If it works well (or mostly well), I'm fine to use hacks to make it work.

Let me know how it goes!

Andarist avatar Mar 22 '22 12:03 Andarist

I was also able to hack around Global issue: https://codesandbox.io/s/emotion-issue-template-forked-vec80x

This could be used together with a bundler. You could create a custom module that would look something like this:

import { Global as EmotionGlobal, useTheme, __unsafe_useEmotionCache  } from '@emotion/react'
import { serializeStyles  } from '@emotion/serialize'
export * from '@emotion/react'

function ServerGlobal({ styles, cache }) {
  let serialized = serializeStyles([styles], undefined, useTheme());
  let serializedNames = serialized.name;
  let serializedStyles = serialized.styles;
  let next = serialized.next;
  while (next !== undefined) {
    serializedNames += " " + next.name;
    serializedStyles += next.styles;
    next = next.next;
  }

  let shouldCache = cache.compat === true;

  cache.insert(
    ``,
    { name: serializedNames, styles: serializedStyles },
    cache.sheet,
    shouldCache
  );
  return null;
}

function Global(props) {
  const cache = __unsafe_useEmotionCache();
  if (cache.server) {
    return <ServerGlobal {...props} cache={cache} />;
  }

  return <EmotionGlobal {...props} />;
}

and alias @emotion/react to this custom module. This way everything would be "directly" imported from @emotion/react except Global. This would actually import this "wrapper" that would conditionally use different logic for your "server rendering".

I mostly present this as a workaround. I would like to figure out a better, builtin, solution - but at the moment I'm not sure how to best approach that.

Andarist avatar Mar 29 '22 08:03 Andarist

I'm still debugging this, it seems like @mui components do not use the cache I provided even I did as the mui document told 😂

Jack-Works avatar Mar 29 '22 09:03 Jack-Works

Do you perhaps have a repro case?

Andarist avatar Mar 29 '22 09:03 Andarist

Still debugging, not having a small repro. We have @mui and tss-react. Now I can get the CSS text from tss-react by your hack. @mui documentation said they use <CacheProvider> exported from @emotion/react but when I passed in, it does not use my own cache.

Jack-Works avatar Mar 29 '22 09:03 Jack-Works

image

interesting, if I add @mui in your hack example, it works

Jack-Works avatar Mar 29 '22 10:03 Jack-Works

Oh I know the reason, it's because we're using React 18!

Jack-Works avatar Mar 29 '22 10:03 Jack-Works

Same code, works in React 17 but not in 18

React 17: https://codesandbox.io/s/emotion-issue-template-forked-ih4od6?file=/src/main.jsx React 18: https://codesandbox.io/s/emotion-issue-template-forked-dn5dqr?file=/src/main.jsx

Jack-Works avatar Mar 29 '22 10:03 Jack-Works

Ah, yes - this is something that I've meant to mention here. That those presented workarounds only work in React 17 (well, the hack for Global should actually work everywhere) and that I need to figure out how to best handle this in React 18 (and if we figure this out then the same builtin solution would work also for React 17).

This is basically the same issue as with Global - the initial workaround for "scoped" styles doesn't work because Global injects styles from within useLayoutEffect. In React 18 we are using React.useInsertionEffect to inject "scoped" styles and thus Global and other stuff work in a similar way and thus the initial workaround doesn't work anymore with React 18 at all (because server-like rendering doesn't execute effects at all).

So really I see the only 2 options here to solve this in the Emotion (if we decide to fix it):

  • remove optimizations relying on stripping away the "server-only" code in the browser bundles (I'm not super keen on this)
  • figure out how to customize the behavior on the cache level, preferably with some kind of a dependency injection~ so the different "path" can be injected without affecting the common use case. This is somewhat problematic because the cache is now React-agnostic and we would have to figure out how to customize it with some React-specific code

One thing that could potentially help you solve this right now:

  • use patch-package to remove package.json#browser from our packages
  • move the "server-rendering" to a web worker

This, in theory, should "just work" because you would use files that are capable of handling both the browser and the server logic in our render functions and we wouldn't resolve to browser-specific path because typeof document check that we are using would assume that it's not running in a browser environment and as such it should choose the "server path"

Andarist avatar Mar 29 '22 11:03 Andarist

Thanks for your help, I guess a worker will be the easiest way to do this. I'll try this tomorrow

Jack-Works avatar Mar 29 '22 13:03 Jack-Works

@Andarist we are running into the same issue (I think). we are essentially calling renderToStaticMarkup/renderToString from react-dom/server to generate html to provide to a library. in react 17, this works fine. in react 18 it doesn't at all.

when you say use patch package to remove package.json#broswer, can you help me understand what you mean more. I use patch-package but idk what I am meant to modify.

jeffshaver avatar Mar 29 '22 21:03 jeffshaver

I mean that you can try removing such entries from our package.json files: https://github.com/emotion-js/emotion/blob/2d3d7dd1d8b0de9dbfd50e42ee716ac00fb70ecd/packages/react/package.json#L6-L9

Please note that even if you do so you would still have to execute renderToStaticMarkup/renderToString in an environment without the global document object.

Andarist avatar Mar 29 '22 22:03 Andarist

thanks for the info!

this doesn't seem like a viable workaround for our use case.

the basic flow is that the library gives us a callback that we return html from. that callback happens synchronously. so I don't think we could move it into a worker.

are there any other options to solve this that wouldn't require moving the html generation into a worker?

jeffshaver avatar Mar 29 '22 22:03 jeffshaver

I think I tried basically everything on in the ssr page of the docs. renderStylesToString and what not. I tried making a cache and providing that to the cache provider.

jeffshaver avatar Mar 29 '22 23:03 jeffshaver

also, I know that react 18 just came out. so if we need to just kind of give it time, that's totally fine. just was looking into the issue I was having while trying to upgrade. I don't want to rush anyone unnecessarily. I don't NEED to upgrade to react 18 immediately.

jeffshaver avatar Mar 29 '22 23:03 jeffshaver

the basic flow is that the library gives us a callback that we return html from. that callback happens synchronously. so I don't think we could move it into a worker.

A synchronous XHR call to a worker? 🤣

On a more serious note, I would love to provide a proper solution for this BUT I think that we need to first implement support for new streaming APIs that are now available in React 18. This is a more pressing issue and one that might influence the solution for this issue here.

Unfortunately, I don't have any ETA for this streaming APIs work - we've talked about the implementation details for this a couple of months back with Mitchell but I never got enough time and mental capacity to start working on it. I plan to work on it... but I can't promise any deadlines.

Andarist avatar Mar 30 '22 08:03 Andarist

@Andarist thanks for the help! we will just wait on the upgrade for now. I haven't contributed to this project before, but if you think there are tasks that we could assist with in anyway I wouldn't be opposed to trying.

jeffshaver avatar Mar 30 '22 11:03 jeffshaver

One thing that could potentially help you solve this right now:

  1. use patch-package to remove package.json#browser from our packages
  2. move the "server-rendering" to a web worker

I did both, but it still cannot render @mui styles. I guess it still using useInsertionEffect to inject the style. I'll try to investigate this today.

Jack-Works avatar Apr 01 '22 04:04 Jack-Works

image

.inserted is empty. Still investigating...

Jack-Works avatar Apr 01 '22 06:04 Jack-Works

@Jack-Works do you have time for a quick call or something? we could potentially hash it out quickly

Andarist avatar Apr 01 '22 07:04 Andarist

image

image

I think I find it out! (But it does not match the source code in GitHub, maybe it because webpack did something)

@Jack-Works do you have time for a quick call or something? we could potentially hash it out quickly

Of course! How should I contact you?

Jack-Works avatar Apr 01 '22 07:04 Jack-Works

Ok, it is because I forgot to remove the "browser" field in the package.json of @emotion/styled. Now it works to get some CSS output, (but the render is still broken).

Jack-Works avatar Apr 01 '22 07:04 Jack-Works

@Jack-Works you can DM me on Twitter: https://twitter.com/AndaristRake

Andarist avatar Apr 01 '22 07:04 Andarist