Emotion: @emotion/cache browser specific module breaks SSR

Created on 27 Feb 2019  Â·  20Comments  Â·  Source: emotion-js/emotion

  • emotion version: 10.0.7
  • react version: 16.8.0

Relevant code:
https://github.com/emotion-js/emotion/blob/master/packages/cache/src/index.js#L69

What you did:
Tried to SSR using static-site-generator-webpack-plugin.

What happened:

ERROR in ReferenceError: document is not defined at createCache
(webpack:////node_modules/@emotion/cache/dist/cache.browser.esm.js?:109:38)

Problem description:
When @emotion/cache gets imported by webpack with target: web it fails due to the browser specific module not including the check for typeof document !== 'undefined'.
As mentioned in https://github.com/emotion-js/emotion/issues/1113 this is a known issue and setting target: node is not an adequate solution.

Suggested solution:
Drop the browser specific version of the module.

Most helpful comment

Thank you for this thread!

This was actually a big problem for us! We wanted to use Emotion 10 together with ReactJS.NET for using React components inside Razor pages in ASP.NET. The problem here is we can't use target "node" because the V8 engine it is running can't understand it (no support for require etc). So we need to use target: "web" but still enable SSR.

By manually deleting all the browser: settings from each emotion package.json like @rdadoune suggested we got it working!

So there are definately cases where a check should be made even if it is the browser specific module.

Follow up findings:

I could also make it work without the monkey patch by setting (in webpack.config.js):

 resolve: {
      aliasFields: ["module"]
},

All 20 comments

The root cause of this is an optimisation done in the module bundler preconstruct that replaces "typeof document" with "object" and is then removed by a minifier that does dead code removal. This is definitely the intended behaviour but perhaps requires some rethinking as its intention probably wasn't to make it impossible to use static-site-generator-webpack-plugin amongst others.

As mentioned in #1113 this is a known issue and setting target: node is not an adequate solution.

Isnt it? Could you prepare a simple repository reproducing the issue with your setup? I could take a look then.

Could you prepare a simple repository reproducing the issue with your setup? I could take a look then.

Absolutely, here it is https://github.com/GU5TAF/emotion-cache-ssr-error

Just yarn && yarn start and you should be good to go.
Check the console in the browser and you'll find Uncaught ReferenceError: require is not defined because we set the target to node but are now running the bundle in a browser.

I had the same problem, I wrote a quick monkey patch to get my build working. This script can be used post install or pre webpack. It basically unsets the "browser" mapping in the package.json of every @emotion package.

const fs = require('fs');
const { sync } = require('glob');

sync('./node_modules/@emotion/*/package.json').forEach(src => {
  const package = JSON.parse(fs.readFileSync(src, 'utf-8'));
  const browser = package.browser;
  delete package.browser;
  if (browser) {
    package._browser = browser;
  }
  fs.writeFileSync(src, JSON.stringify(package, null, 2))
});

@GU5TAF it's the problem of your setup. I'm not sure how static-site-generator-webpack-plugin should be used, so I can't quite tell you how you should fix it.

The problem (the require call) is coming from react-dom/server which you try to ship to the browser by including it in your entry point with import statement.

You can check out that this is not emotion-related by removing all emotion stuff from your demo - it still won't work.

The problem (the require call) is coming from react-dom/server which you try to ship to the browser by including it in your entry point with import statement.

My apologies @Andarist, there was indeed a typo in my call to hydrate but apart from that I can assure you that setting the target to web and dropping emotion it'll work just fine.

I created a new branch called working-example where I do just that so you can take it for a spin.

But this is again caused by how your entry is structured. Your entry gets shipped to the browser and you include in it react-dom/server (which is wrong, it won't be used in the browser at all but you ship the whole server renderer nevertheless). The problem is that u want to handle both server & client paths through a single webpack config - it can't work, at least not with a structure which you have there right now.

it can't work, at least not with a structure which you have there right now.

It can't work with emotion or it can't work in general?
As I said, there is now a branch that is fully working, static render + hydrate in the browser.

You're correct that react-dom/server will be shipped to the browser unnecessarily, however, react-dom include a browser version of react-dom/server for that very reason. When bundled with --mode=production, react-dom comes in at 105.51 KB and react-dom/server comes in at 18.75 KB which is only 7 KB gzipped. An unnecessary 7 KB for sure but not the end of the world in my opinion.

The problem with the environment I was using is that it's not for the browser or for node, the script needs to be run ad-hoc via Google V8 JS engine.

@rdadoune that sounds like a challenging environment to develop for 😅thanks for the monkey patch!

@Andarist I agree that it would be even more optimal for the resultant bundle to run webpack twice, once to generate the html, storing only html files and then once more to create the browser bundle.

However, this approach with a bundle that can run in both contexts isn't that uncommon and all it would take to support it completely is for emotion not to over optimize the browser bundle.

The monkey patch @rdadoune provided us with fixes the issue, once run the current broken example in master just works. Surely this is desirable behaviour?

I've applied it as a pre build step in a new branch called rdadoune if you're curious.

I'm seeing the same error when using html-webpack-plugin with a custom template that does React SSR. Setting the webpack target to node is not an option with this plugin since the HTML & JS are generated using the same webpack config.

Conversely, and maybe this should be tracked as a separate issue - when doing SSR in node, with a mocked document object (JSDOM), @emotion/cache does not populate the cache with inserted rules, which prevents extraction of critical styles. This appears to be due to the isBrowser check resolving to true. Mocking document (and window) is necessary when using 3rd-party React components which assume they are running in a browser and access those browser globals.

From what I know - most SSR solutions use different webpack settings for server and client builds and this is how this problem should ultimately be solved. If any particular SSR-solution doesn't differentiate this, it really should - so I would advise reporting this to those SSR-solutions as a thing they should handle.

Thank you for this thread!

This was actually a big problem for us! We wanted to use Emotion 10 together with ReactJS.NET for using React components inside Razor pages in ASP.NET. The problem here is we can't use target "node" because the V8 engine it is running can't understand it (no support for require etc). So we need to use target: "web" but still enable SSR.

By manually deleting all the browser: settings from each emotion package.json like @rdadoune suggested we got it working!

So there are definately cases where a check should be made even if it is the browser specific module.

Follow up findings:

I could also make it work without the monkey patch by setting (in webpack.config.js):

 resolve: {
      aliasFields: ["module"]
},

@johot great find, thanks for the follow up.

@johot Thank you! I've spent more time than I'd like to admit on this. Not sure what it does, since the docs are not that clear about it, but for now I'm just happy it works! Thanks again!

I am having a similar issue with @emotion/[email protected]. I am working on a component library created using create-react-library, and using @emotion/react for styling. It works fine in many contexts, but when we try to use components from this library in a Next.js app that uses SSR, we get a similar error to the one mentioned above:

ReferenceError: window is not defined
    at Object.<anonymous> (.../node_modules/@emotion/react/dist/emotion-react.browser.esm.js:310:37)

which I believe has a similar cause, since our compiled component library has the following checks throughout (from Emotion):

var isBrowser = "object" !== 'undefined';

which leads the code down paths where things like window and document are expected to exist, which they won't in the SSR case. I am able to work around this issue by importing the components in a try/catch, or by using Next.js's dynamic imports, but it would be great if there was a solution that did not require workarounds. I am not sure if the issue in our case is related to some configuration (to microbundle, webpack, babel, typescript, etc.), or whether it is a genuine bug. Unfortunately, because of the nature of the project, I don't have access to most of those configs, and since it is a private repository, I don't have an example repo on hand to share, so I apologize for that. My understanding is that Emotion v11 with Next.js SSR "just works", but perhaps that is only if I am using Emotion directly in a Next.js app, as opposed to my case where I am using Emotion in a standalone component library, and then installing that library as a dependency of a Next.js app. Do you have any suggestions as to how to move forward?

@el-ethan it looks like your bundler is configured to consume browser files - so it's not a surprise that this breaks in SSR. In webpack when bundling for SSR you should configure target: 'node' and this is what most of the webpack-based tools (like Next.js) do. If those files have been loaded by Next's SSR then there is surely something configured wrongly. If you share a runnable repro case I could take a look at your problem.

I had the same problem with Gatsby in Theme UI docs. (Worked around it already.)

it looks like your bundler is configured to consume browser files - so it's not a surprise that this breaks in SSR.

Is this still true? — Node 15 consumes esmodules, and the window will still be undefined there, and I think that even in previous version people and metaframeworks _do_ configure bundlers to consume esm.

Esm files !== browser files. The comment was referring to bundler using package.json#browser even though the target environment was node

Oh, sorry. I missed that emotion-react.esm.js exists and has var isBrowser = typeof document !== 'undefined'; instead of isBrowser = "object" !== 'undefined';. Sorry for necroposting. Definitely a fault on bundler's config side.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

meebix picture meebix  Â·  3Comments

AlexanderProd picture AlexanderProd  Â·  3Comments

tkh44 picture tkh44  Â·  3Comments

mitchellhamilton picture mitchellhamilton  Â·  3Comments

desmap picture desmap  Â·  3Comments