emotion-js/emotion

Hydration errors in development for Next.js app with MUI Material v5 after updating emotion dependencies

ddembo opened this issue · 20 comments

Current behavior:

We are getting hydration errors in development on pages that render components which are styled MUI components, after updating the following dependencies:

  • @emotion/cache from 11.11.0 to 11.13.0
  • @emotion/react from 11.11.4 to 11.13.0
  • @emotion/styled from 11.11.5 to 11.13.0
Warning: Prop `className` did not match. Server: "MuiTableCell-root MuiTableCell-body MuiTableCell-sizeSmall e18whkzw0 mui-az1ynu-MuiTableCell-root-TableCellLinkContainer" Client: "MuiTableCell-root MuiTableCell-body MuiTableCell-sizeSmall e18whkzw0 mui-1odz46y-MuiTableCell-root-TableCellLinkContainer"

Notably, we did not update MUI or any related dependencies at the same time.

It only affects components created using the styled helper, e.g.:

import styled from "@emotion/styled";
import TableRow from "@mui/material/TableRow";

const StyledTableRow = styled(TableRow)({
  "&:last-child > th, &:last-child > td": {
    borderBottom: 0,
  },
});

export default StyledTableRow;

It seems to be a problem with one of those packages' underlying dependencies, because the issue disappears when we use the old package-lock.json, but not if we nuke and recreate it.

I've been trying to identify the root cause but I'm finding it difficult, I'm not very familiar with the internals of emotion. I think it could be a problem with the serialiser, because the client <style> tags being inserted look to me like the data-emotion attributes are missing the serialized classnames that typically seem to follow the cache prefix mui, e.g.:

image

It could also be a problem with the hash package, since the hashed part of the className is different.

The issue is not present in production builds of the app.

To reproduce:

CSB: TODO later if I have some more time

  1. Make a styled MUI component as described above, put it on a Next.js page.
  2. Visit that route in a browser, open devtools.

Expected behavior:

Classnames should match on both client & server, no hydration errors.

Environment information:

"react": "^18.3.1",
"@emotion/react": "^11.13.0",

"@emotion/cache": "^11.13.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.13.0",

"@mui/material": "^5.16.4",
"@mui/material-nextjs": "^5.16.4",

same here, we need it to be fixed.

I need a runnable repro case to investigate this.

I can get that over to you tomorrow.

I'd appreciate that, thanks!

interestingly, when I shrink the project to bare minimum, the issue is gone, I will need to do more research.

FWIW, I am getting this problem with:

"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",

I think this is introduced as an issue with this change. The server now defaults to using emotion-serialize.cjs.js while webpack will automatically use development.cjs.js version. The non-development version excludes sourceMaps explicitly from the serialization algorithm while the development version does not.

It doesn't seem reasonable to assume that users will know to add --conditions to the node process to fix this development issue, especially when it is still an experimental flag.

Setting an override in my package.json fixed this for me:

    "overrides": {
        "@emotion/serialize": "1.2.0"
    }

@mridgway I can confirm I have this issue even with 1.2.0

▶ cat pnpm-lock.yaml | grep @emotion/serialize
      '@emotion/serialize':
  '@emotion/serialize@1.2.0':
      '@emotion/serialize': 1.2.0
      '@emotion/serialize': 1.2.0
  '@emotion/serialize@1.2.0':
      '@emotion/serialize': 1.2.0
      '@emotion/serialize': 1.2.0

pnpm-lock.yaml.zip

It doesn't seem reasonable to assume that users will know to add --conditions to the node process to fix this development issue, especially when it is still an experimental flag.

This is news to me. I totally considered this a stable feature. It's mentioned multiple times in other parts of the docs and is never guarded with the experimental warning at those places. On top of that, node.js doesn't emit any experimental features warnings when you use custom conditions so it's not apparent at all that it's experimental.

That said, I don't know how we are supposed to deal with this. The environment needs to be in a cohesive state. What kind of setup are you using? Is your server created manually?

I think this is introduced as an issue with this change. The server now defaults to using emotion-serialize.cjs.js while webpack will automatically use development.cjs.js version. The non-development version excludes sourceMaps explicitly from the serialization algorithm while the development version does not.

It doesn't seem reasonable to assume that users will know to add --conditions to the node process to fix this development issue, especially when it is still an experimental flag.

Setting an override in my package.json fixed this for me:

    "overrides": {
        "@emotion/serialize": "1.2.0"
    }

I can confirm that the override works for me, after override it back to 1.2.0, and redo npm install, the warning is gone.

I think this is a red herring. My current version of code uses @emotion/serialize@1.1.4 in my pnpm-lock.yaml file.
The only emotion packages I use/specify in my package.json files are:

"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",

I think this is introduced as an issue with this change. The server now defaults to using emotion-serialize.cjs.js while webpack will automatically use development.cjs.js version. The non-development version excludes sourceMaps explicitly from the serialization algorithm while the development version does not.

It doesn't seem reasonable to assume that users will know to add --conditions to the node process to fix this development issue, especially when it is still an experimental flag.

Setting an override in my package.json fixed this for me:

    "overrides": {
        "@emotion/serialize": "1.2.0"
    }

Using MUI Material v5 with Nextjs Pages router and got this exact problem after updating emotion dependencies.

While I don't have "@emotion/serialize" directly in my package.json overriding "@emotion/serialize" as mentioned, running install and restarting the server fixed it for me

Once I get the repro case of the problem, I could investigate this

@Andarist I've made a minimal reproduction of the issue here. The error is slightly different (complains about table tag instead of classname) but it looks like the same problem with malformed style tags in the DOM.

Thank you for looking into this, please let me know if you need any more details.

The serialization issue is also reproducible in the basic next.js create-next-app example:

npx create-next-app --example with-emotion repro
cd repro
npm run dev

Screenshot 2024-07-30 at 8 15 04 PM

After adding to the package.json:

  "overrides": {
    "@emotion/serialize": "1.2.0"
  },

Screenshot 2024-07-30 at 8 20 56 PM

As an additional data point, I've been seeing these errors since MUI 5.16.0 and have been pinned on 5.15.21 until I had time to put together a reproduction. I got the emotion changes today and am using @emotion/serialize: 1.3.0. So it's possible that there are several overlapping issues here causing the same failures, as suggested by @jakeleventhal.

Is there a way to actually fix it without overriding dependencies and downgrading packages?