Emotion: Client Side className does not change when server side rendered

Created on 13 Nov 2017  路  10Comments  路  Source: emotion-js/emotion

  • emotion version: 8.0.10
  • react version: 16.0.0

Let me start by saying I am doing something a bit unorthodox. However, I believe that what I am doing is technically _correct_ expected behavior as far as React goes.

We have a service that leverages server side rendering for SEO purposes using React 16. We then leverage client side rendering for attaching any client side behaviors (onclicks, etc). While we are developing pages for this app, we have traditionally relied on ReactDOM.render on the client side over ReactDOM.hydrate. However, when running the app in "production mode" (ie, not development mode), we use hydrate. The reason we use render is because we use webpack to watch for changes in our client code and rebundle the new assets. This, coupled with ReactDOM.render has worked nicely because if we make a change to one of our components, we don't need to restart our server and instead can leverage the fact that render's interpretation of the DOM will take priority over the server side rendered interpretation of it. This makes "development mode" much nicer as we don't need to restart our server every time we make a change to our React components. Webpack just rebundles, we hit refresh, and the client side bundle's version of the DOM matches our latest change.

That said, I've experimenting with switching over from our SCSS based CSS over to Emotion. When doing so, I noticed some unexpected behavior.

Relevant code.

// Client.js

const Demo = styled.div`
  background-color: yellow;
  color: white;
  display: block;
  min-height: 50px;
  min-width: 50px;
`;

// ... more code happens here ...

import { render } from 'react-dom';
import { hydrate as emotionHydrate } from 'react-emotion';

render(<Demo>Hello world</Demo>, document.getElementById('root'));
emotionHydrate(window.__ids__);
// server.js

const { html, ids, css } = extractCritical(renderToString(component));

res.send(`
<style type="text/css">${css}</style>
<div id="root">${html}</div>
<script>
window.__ids__ = ${serialize(ids)};
</script>
`);

What you did:

The above code was being rendered into a component that is server side rendered. When its server side rendered, it writes the appropriate HTML into the page and renders the page with the styles as expected.

<div class="css-4ueu7r">Hello world</div>

However, if I change one of the styles, say the background-color from yellow to blue:

const Demo = styled.div`
  background-color: blue;
  color: white;
  display: block;
  min-height: 50px;
  min-width: 50px;
`;

The client bundle is recreated through webpack, however when I refresh the page, I see no styles applied. The following error is output in the browser:

warning.js:33 Warning: Prop className did not match. Server: "css-4ueu7r" Client: "css-y1patc"

The element seems to have no styles applied now. When I inspect the DOM, the element still has the same class value css-4ueu7r. However, if I manually change the class attribute to css-y1patc, I see the blue background color and all the other styles.

I tried configuring the our development mode such that it doesn't call emotionHydrate(window.__ids__); (see above) but that didn't seem to make a difference. So as best I can tell, emotion will create the styles in the <style /> element but it doesn't update the class on the element when there's a mismatch between the server rendering and the client rendering.

The "best" workaround I've come up with is just disabling SSR while in dev mode. However, I have really mixed feelings about doing that and would much prefer SSR still be enabled while in dev mode, just allowing the client side rendering to always take priority over the SSR one.

Most helpful comment

@ericmasiello i know it's been a while since your first post, but still, i want to leave this here in case anyone has the same issue 馃槃

I was hitting the same issue. I have a setup using express-dev-middleware for dev mode, and when a request lands, I fetch some data and dynamically render a react component to string using renderToString

So, when my dev environment is running, if I make a change to a component (style, content, etc), hot-module-replacement will push the changes to the client without problems.

So far so good 馃槃

But then, if reload the page, making a request to our server and essentially re-rendering our component the component will render the same content as before.

馃

This is because require will still have the previous file content stored in memory (So not to require the file again)
So we need just to invalidate require cache (living on require.cache) for the specific file we changed.

(In my case, I took the short path and invalidate the whole tree of my 'client-side' files, that live inside ../client)

Server.js

const clearRequireComponentCache = () => {
  const key = path.resolve(__dirname, '../client')
  Object.keys(require.cache).forEach(file => {
    if (file.startsWith(key)) {
      require.cache[file] = undefined 
    }
  })
}

const routeHandler = (res) => {
  const Layouts = require('../client/layout').default 
  const body = renderToString(<Layouts data={data} />)
  const { html, ids, css } = extractCritical(body)
  res.send(
    functionThatRetunsAnHTMLTemplate({
      idsToRehydrate: ids,
      cssToInject: css,
      body: html,
    })
  )

  if (config.isDev) {
    clearRequireComponentCache()
  }
}

All 10 comments

Wow, thanks for this awesome report. Are you using the babel plugin? If so, can you please post it?

Thanks :)

I am using the babel plugin. Here's my .babelrc config:

{

  "plugins": [
    "emotion",
    "transform-object-rest-spread"
  ],
  "presets": ["react", "es2015"],
  "env": {
    "node": {
      "plugins": [
        "emotion",
        [
          "babel-plugin-transform-require-ignore",
          {
            "extensions": [".css", ".scss"]
          }
        ]
      ]
    }
  }
}

I'm pretty sure this is expected behavior with react(as it outlines in the docs). If I understand correctly, react 16 does the same hydration with render and hydrate. I just checked and it doesn't update in a basic example with only react and react-dom using renderToString and render with a different className so I don't think this is an issue with emotion.(check in the DOM for the name of the class)

React 16.2.0 hydrate

If you intentionally need to render something different on the server and the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like this.state.isClient, which you can set to true in componentDidMount(). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.

@ericmasiello encoutered the same issue, did you find a solution? 馃槷 ??

I think I ended up aborting using render when doing SSR. It doesn鈥檛 seem to be supported. Instead when in dev mode, I disable SSR and just render using ReactDOM.render as if it were a client side only app.

@ericmasiello i know it's been a while since your first post, but still, i want to leave this here in case anyone has the same issue 馃槃

I was hitting the same issue. I have a setup using express-dev-middleware for dev mode, and when a request lands, I fetch some data and dynamically render a react component to string using renderToString

So, when my dev environment is running, if I make a change to a component (style, content, etc), hot-module-replacement will push the changes to the client without problems.

So far so good 馃槃

But then, if reload the page, making a request to our server and essentially re-rendering our component the component will render the same content as before.

馃

This is because require will still have the previous file content stored in memory (So not to require the file again)
So we need just to invalidate require cache (living on require.cache) for the specific file we changed.

(In my case, I took the short path and invalidate the whole tree of my 'client-side' files, that live inside ../client)

Server.js

const clearRequireComponentCache = () => {
  const key = path.resolve(__dirname, '../client')
  Object.keys(require.cache).forEach(file => {
    if (file.startsWith(key)) {
      require.cache[file] = undefined 
    }
  })
}

const routeHandler = (res) => {
  const Layouts = require('../client/layout').default 
  const body = renderToString(<Layouts data={data} />)
  const { html, ids, css } = extractCritical(body)
  res.send(
    functionThatRetunsAnHTMLTemplate({
      idsToRehydrate: ids,
      cssToInject: css,
      body: html,
    })
  )

  if (config.isDev) {
    clearRequireComponentCache()
  }
}

Hi @ericmasiello

I am also facing this issue with emotion and react. I am using react 15.4 in my production code.

While rendering the view using renderToString() function, I am using emotion's renderStylesToString function also. Everything works great with SSR.

The issue arises when we use react and emotion on the client side as well. The class names of each div are changed and the CSS is applied again. This is clearly visible if we add any drop-down animation effect of content. The animation is applied twice - once while SSR and then client side.

I am aware of the emotion hydrate function but the documentation says not to use it if using renderStylesToString.

However, this doesn't seem to be an issue with emotion but react. React 16 has a hydrate function which will preserve the dom. But I am using react 15.

Is there any solution to this problem?

@dhruv2204 sorry i never found a solution so I'm not sure.

Is there a way to prevent emotion to run on the client side?

Was this page helpful?
0 / 5 - 0 ratings