React + Material-UI - 警告:Prop className 不匹配

86

由于在Material-UI组件的客户端和服务器端渲染样式时,className被分配方式不同,导致我对客户端和服务器端渲染之间的差异感到困惑。

在第一次加载页面时,classNames被正确地分配,但是在刷新页面后,classNames不再匹配,因此组件失去了其样式。这是我在控制台上收到的错误消息:

警告:Prop className did not match. Server: "MuiFormControl-root-3 MuiFormControl-marginNormal-4 SearchBar-textField-31" Client: "MuiFormControl-root-3 MuiFormControl-marginNormal-4 SearchBar-textField-2"

我遵循了Material-UI TextField的示例文档和它们附带的Code Sandbox示例,但我似乎无法弄清楚是什么原因导致服务器和客户端的classNames之间存在差异。

当我添加带有删除 'x' 图标的Material-UI Chips时,也遇到了类似的问题。在刷新后,'x'图标呈现为1024px的巨型宽度。造成这个问题的根本原因相同,即该图标未接收到正确的样式类。

在Stack Overflow上有一些关于为什么客户端和服务器可能会以不同的方式渲染classNames的问题(例如需要升级到@Material-UI/core版本^1.0.0,在使用自定义server.js和在setState中使用Math.random),但这些都不适用于我的情况。

我不知道是否可以参考此Github讨论,但可能不行,因为他们正在使用Material-UI的beta版本。

重现最小步骤:

创建项目文件夹并启动Node服务器:

mkdir app
cd app
npm init -y
npm install react react-dom next @material-ui/core
npm run dev

编辑 package.json:

在 'scripts' 中添加:"dev": "next",

app/pages/index.jsx:

import Head from "next/head"
import CssBaseline from "@material-ui/core/CssBaseline"
import SearchBar from "../components/SearchBar"

const Index = () => (
  <React.Fragment>
    <Head>
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"
      />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <meta charSet="utf-8" />
    </Head>
    <CssBaseline />
    <SearchBar />
  </React.Fragment>
)

export default Index

app/components/SearchBar.jsx:

import PropTypes from "prop-types"
import { withStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"

const styles = (theme) => ({
  container: {
    display: "flex",
    flexWrap: "wrap",
  },
  textField: {
    margin: theme.spacing.unit / 2,
    width: 200,
    border: "2px solid red",
  },
})

class SearchBar extends React.Component {
  constructor(props) {
    super(props)
    this.state = { value: "" }
    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  handleChange(event) {
    this.setState({ value: event.target.value })
  }

  handleSubmit(event) {
    event.preventDefault()
  }

  render() {
    const { classes } = this.props
    return (
      <form
        className={classes.container}
        noValidate
        autoComplete="off"
        onSubmit={this.handleSubmit}
      >
        <TextField
          id="search"
          label="Search"
          type="search"
          placeholder="Search..."
          className={classes.textField}
          value={this.state.value}
          onChange={this.handleChange}
          margin="normal"
        />
      </form>
    )
  }
}

SearchBar.propTypes = {
  classes: PropTypes.object.isRequired,
}

export default withStyles(styles)(SearchBar)

请在浏览器中访问页面 localhost:3000 并查看如下内容:

TextField 组件周围有红色边框

刷新浏览器并查看如下内容:

TextField 组件的样式消失了

注意:TextField 周围的红色边框消失了。

相关库:

  • "react": 16.4.0
  • "react-dom": 16.4.0
  • "next": 6.0.3
  • "@material-ui/core": 1.2.0

你在解决这个问题方面有什么进展了吗? - Dhana Krishnasamy
1
@DhanaKrishnasamy - 是的,这些MUI文档解释了如何解决问题。我对Web开发还比较新,所以我不理解MUI文档。最终,我按照builderbook的第一章来集成MUI的客户端和服务器端渲染。请注意,您可以免费在github上查看builderbook代码 - 我最终购买了这本书,花费了20美元,并按照说明进行操作(这至少节省了我一天的时间,也许更多)。 - David
15个回答

73
问题是Next.js中的SSR渲染,在页面渲染之前生成了样式片段。
使用Material UI和Next.js(作者正在使用),添加一个名为_document.js的文件解决了这个问题。
调整了_document.js按照此处建议):
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/styles'; // works with @material-ui/core/styles, if you prefer to use it.
import theme from '../src/theme'; // Adjust here as well

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>
          {/* Not exactly required, but this is the PWA primary color */}
          <meta name="theme-color" content={theme.palette.primary.main} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
  // Resolution order
  //
  // On the server:
  // 1. app.getInitialProps
  // 2. page.getInitialProps
  // 3. document.getInitialProps
  // 4. app.render
  // 5. page.render
  // 6. document.render
  //
  // On the server with error:
  // 1. document.getInitialProps
  // 2. app.render
  // 3. page.render
  // 4. document.render
  //
  // On the client
  // 1. app.getInitialProps
  // 2. page.getInitialProps
  // 3. app.render
  // 4. page.render

  // Render app and page and get the context of the page with collected side effects.
  const sheets = new ServerStyleSheets();
  const originalRenderPage = ctx.renderPage;

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
    });

  const initialProps = await Document.getInitialProps(ctx);

  return {
    ...initialProps,
    // Styles fragment is rendered after the app and page rendering finish.
    styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
  };
};

4
谢谢你将解决方案直接发布到这个帖子中。我自己还没有测试过,但是乍一看它似乎与我最终采用的解决方案等价。 - David
2
非常感谢,它完成了任务。 - Matt Loye
1
这对我很有效。非常感谢您发布这个。 :) - dhellryder
2
这应该是2021年的最佳答案。 - victor.ja
3
你是一个救世主。 - bareMetal
显示剩余3条评论

26

这个问题涉及到MUI使用包含ID的动态类名。来自服务器端呈现的CSS的ID与客户端CSS不同,因此会导致不匹配错误。一个好的开始是阅读MUI SSR文档

如果你在使用nextjs时遇到了这个问题(就像我一样),可以按照MUI团队提供的示例进行操作,这里可以找到:material-ui/examples/nextjs

最重要的部分在"examples/nextjs/pages/_app.js"中:

componentDidMount() {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector('#jss-server-side');
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles);
    }
  }

相关的票据可以在这里找到:mui-org/material-ui/issues/15073

它的作用是删除服务器端呈现的样式表,并将其替换为新的客户端呈现的样式表。


7
对我来说,那些例子中也需要使用_document.js。 - Mārcis P
1
我已经使用了useEffect()而不是componentDidMount()和_document.js,但仍然遇到了问题。 - Meli

21

问题在于服务器端生成了类名,但样式表没有自动包含在HTML中。您需要显式地提取CSS并将其附加到服务器端渲染组件的用户界面中。整个过程在这里解释:

https://material-ui.com/guides/server-rendering/

1
你好, 我按照文档中描述的完全一样的步骤进行了操作。但是服务器和客户端的样式不同,而且它们也没有对齐。控制台显示存在类名不匹配的问题。如果有解决此问题的指针,将非常有帮助。谢谢。 - lekhamani
@lekhamani,如果没有更多信息很难说出问题所在。您可以添加更多详细信息吗? - Dhana Krishnasamy
3
关于Next.js,请看我下面的回答。 - chrisweb
2
我同意你的答案。对于那些喜欢视频解释的人,你可以观看下面的视频:https://www.youtube.com/watch?v=mtGQe7rTHn8 - Dijiflex
注意:对于阅读chrisweb上面的评论“对于Next.js,请参见我的下面的答案”,我已经接受了这个答案,所以现在应该是“上面”。 - David
1
这是正确的答案,但对于需要快速修复的人,可以将MUI组件包装在<NoSSR>中,以快速从SSR渲染中删除它:https://v4.mui.com/components/no-ssr/#no-ssr - jvhang

16
这里还有一个重要的问题需要注意:Material UI V4 不兼容React Strict Mode。版本5将采用Emotion样式引擎来实现Strict mode兼容性。
在此之前,请务必禁用React Strict Mode。如果您正在使用Next.js,且使用create-next-app创建应用程序,则该模式默认开启
// next.config.js
module.exports = {
  reactStrictMode: false, // or remove this line completely
}


谢谢,这是我问题的根本原因。 - Meli
@fluggo 谢谢!!! 你是怎么找到这个解决方案的? - Saber
@Arvand 一些深入的挖掘。 - mrdecemberist

14

我使用Next.js和styled-component时遇到了同样的问题,使用Babel进行转译时实际上客户端和服务器端的类名是不同的。

在.babelrc文件中添加以下内容即可解决:

{
"presets": ["next/babel"],
"plugins": [
    [
      "styled-components",
      { "ssr": true, "displayName": true, "preprocess": false }
    ]
]
}

3
我没有使用 styled-components。 - Jamie Hutber
1
@JamieHutber我也喜欢这种方式,但我想知道为什么这样做有效。 - Laode Muhammad Al Fatih
1
.babelrc文件在哪里? - Jatin Hemnani
@JatinHemnani 如果你没有 .babelrc 文件,只需在项目的根目录中创建它。这里有一个示例 - Hrun
1
成功了。你救了大家一命。 - spwisner
有没有办法通过 next 配置文件而不是 babel 来实现这一点?我需要为 styled components 设置默认值,并使用 next-transpile-modules 加载我的一些自定义设计主题到我的项目中。 - roninMo

12

我在 Material-ui V5 中遇到了这个问题。解决此问题的方法是确保类名生成器在服务器和客户端上的行为相同,因此请在您的 _app.js 中添加以下代码:

import { StylesProvider, createGenerateClassName } from '@mui/styles';

const generateClassName = createGenerateClassName({
  productionPrefix: 'c',
});

export default function MyApp(props) {
  return <StylesProvider generateClassName={generateClassName}>...</StylesProvider>;
}


1
这个解决方案真是救了我的一天。我在使用 MUI 版本 5 时遇到了一个问题,一直在寻找解决方法。MUI 文档提供了一个脚手架项目,位于 https://github.com/mui-org/material-ui/tree/HEAD/examples/nextjs,但是该示例需要进行上述更正才能消除 className 不匹配的错误!正如作者所提到的,return 函数返回的所有内容都需要包裹在 <StylesProvider generateClassName={generateClassName}>....</StylesProvider> 中。感谢 @user9019830。 - James
这也解决了我在mui v5中遇到的错误。你不知道我有多感激你。我赞同这个观点。他们需要更新示例以包括此内容。非常感谢! - Journey_Man
但我没有使用那个单独的包@mui/styles,我正在使用@mui/material,在这种情况下该怎么解决? - Md. A. Apu

5

// 1 . Warning: prop classname did not match. Material ui   with   React  Next.js

// 2 . Use your customization  css here
const useStyles = makeStyles((theme) => ({

    root: {
        flexGrow: 1,
    },

    title: {
        flexGrow: 1,
    },
    my_examle_classssss: {
        with: "100%"
    }

}));


// 3 . Here my Component    
const My_Example_Function = () => {

    const classes = useStyles();

    return (
        <div className={classes.root}>
            <Container>
                <Examle_Component>    {/*  !!! Examle_Component  -->  MuiExamle_Component*/}

                </Examle_Component>
            </Container>
        </div>
    );
}

export default My_Example_Function


// 4. Add  name parameter to the makeStyles function   

const useStyles = makeStyles((theme) => ({

    root: {
        flexGrow: 1,
    },

    title: {
        flexGrow: 1,
    },
    my_examle_classssss: {
        with: "100%"
    },
}), { name: "MuiExamle_ComponentiAppBar" });  

{/* this is the parameter you need to add     { name: "MuiExamle_ComponentiAppBar" } */ }


{/* The problem will probably be resolved     if the name parameter matches the first className in the Warning:  you recive..    


EXAMPLE :

    Warning: Prop `className` did not match. 
    Server: "MuiSvgIcon-root makeStyles-root-98" 
    Client: "MuiSvgIcon-root makeStyles-root-1"


The name parameter will be like this   { name: "MuiSvgIcon" }




*/  }


3

我想分享一个不匹配的案例:

next-dev.js?3515:32 警告:属性className不匹配。服务器端: "MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-12 MuiSwitch-switchBase MuiSwitch-colorSecondary" 客户端: "MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-12 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-checked-13 Mui-checked"

在客户端中,有两个额外的类别,这意味着客户端上的行为是不同的。在这种情况下,该组件不应该在服务器端渲染。解决方案是动态渲染此组件:

export default dynamic(() => Promise.resolve(TheComponent), { ssr: false });

1

这个问题是由Nextjs服务器端渲染引起的。为了解决这个问题,我采取以下步骤:

  1. 创建一个组件来检测是否来自客户端
import { useState, useEffect } from "react";

interface ClientOnlyProps {}

// @ts-ignore
const ClientOnly = ({ children }) => {
  const [mounted, setMounted] = useState<boolean>(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  return mounted ? children : null;
};

export default ClientOnly;

  1. 使用 ClientOnly 组件包装我的页面组件
export default function App() {
  return (
    <ClientOnly>
      <MyOwnPageComponent>
    </ClientOnly>
  );
}


因此,这个想法是,如果是客户端,则只在页面上呈现组件。因此,如果当前的呈现来自客户端,则呈现<MyOwnPageComponent>,否则不呈现任何内容。

1

您可以在任何使用makeStyles的地方添加名称,例如:

const useStyles = makeStyles({
  card: {
    backgroundColor: "#f7f7f7",
    width: "33%",
  },
  title: {
    color: "#0ab5db",
    fontWeight: "bold",
  },
  description: {
    fontSize: "1em"
  }
}, { name: "MuiExample_Component" });

我不确定它是如何工作的,但我在这里找到了它:警告:Prop `className`未匹配~ Material UI css在重新加载时会任意中断


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接