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

React + Material-UI - Warning: Prop className did not match

由于 class 分配的名称不同,我很难处理 Material-UI 组件中客户端和服务器端样式呈现之间的差异。

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

Warning: 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 example docs, and their accompanying Code Sandbox example,但我似乎无法弄清楚是什么导致了服务器和客户端之间的差异class名字。

我在添加带有删除 'x' 图标的 Material-UI 筹码时遇到了类似的问题。 'x' 图标在刷新后呈现出惊人的 1024 像素宽度。同样的潜在问题是该图标没有收到正确的 class 样式。

Stack Overflow 上有几个问题解决了为什么客户端和服务器可能以不同方式呈现 class名称(例如,需要升级到 @Material-UI/core 版本 ^1.0.0,使用自定义 server.js,并在 setState 中使用 Math.random),但其中 none 适用于我的情况。

我不知道 this Github discussion 是否有帮助,但可能不会,因为他们使用的是 Material-UI.

的测试版

最少的重现步骤:

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

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 并看到:

red border around TextField component

刷新浏览器,看到这个:

TextField component's styles are gone

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

相关库:

问题是服务器端生成 class 名称,但样式表不会自动包含在 HTML 中。您需要显式提取 CSS 并将其附加到服务器端呈现组件的 UI。整个过程在这里解释:https://material-ui.com/guides/server-rendering/

我在 Next.js 和样式化组件中遇到了同样的问题,通过 Babel 进行了转译。实际上,class 名称在客户端和服务器端是不同的。

在你的 .babelrc 中写入这个来修复它:

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

此问题与使用包含 ID 的动态 class 名称的 MUI 有关。服务器端呈现的 ID CSS 与客户端 CSS 不同,因此出现不匹配错误。一个好的开始是阅读 MUI SSR documentation

如果您在使用 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

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

在我的例子中,问题的发生是因为客户端代码和服务器端的 webpack 编译模式不同:客户端的包是由 webpack 使用 "production" 模式生成的,而服务器 运行 一些来自为 "development" 优化的包的 SSR 代码。这在 generateAndInjectStyles():

的样式组件中创建了一个不同的 "className" 散列
if (process.env.NODE_ENV !== 'production') dynamicHash = phash(dynamicHash, partRule + i);

所以我的修复只是对齐 webpack 模式。

问题出在Next.js中的SSR渲染,会在页面渲染之前产生样式片段。

使用 Material UI 和 Next.js(作者正在使用),添加名为 _document.js 的文件解决了问题。

调整后_document.js (as suggested here):

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()],
  };
};

我对客户端和服务器的不同类名有疑问。我正在使用 ReactMaterial-UImakeStylesSSR(服务端渲染)。 错误是:

Warning: Prop `className` did not match. Server: "jss3" Client: "App-colNav-3"

我花了几个小时才发现客户端和服务器的 webpack 模式 存在差异。 package.json 中的脚本是:

    "devServer": "webpack --config webpack.server.config.js --mode=production --watch",
    "devClient": "webpack --mode=development --watch",

我把两者都改成development模式后,问题就解决了:)

    "devServer": "webpack --config webpack.server.config.js --mode=development --watch",
    "devClient": "webpack --mode=development --watch",

这里还有一个重要的独立问题:Material UI V4 是 not React Strict Mode compatible. Strict mode compatibility is slated for version 5 with the adoption of the Emotion style engine

在那之前,请务必禁用 React 严格模式。如果您使用 Next.js,这是 turned on by default 如果您使用 create-next-app 创建您的应用程序。

// next.config.js
module.exports = {
  reactStrictMode: false, // or remove this line completely
}

// 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" }




*/  }

我在 Material-ui V5 上遇到了这个问题。解决此问题的解决方案是确保 class 名称生成器需要在服务器和客户端上具有相同的行为。 所以在你的 _app.js:

中添加下面的代码

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

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

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

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

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

我不确定它是如何工作的,但我在这里找到了它:

如果有人在尝试上述解决方案后仍然苦苦挣扎,试试这个

  • 如果您在任何组件或主题中使用了 noSsr 道具,请将其删除。

我在 mui theme 对象中有以下配置,导致了这个问题。

import { createTheme, responsiveFontSizes } from "@mui/material/styles";
let theme = createTheme({
  components: {
    MuiUseMediaQuery: {
      defaultProps: {
        noSsr: true,
      },
    },
  },
  palette: {
    mode: "light",
    common: {
      black: "#000",
      white: "#fff",
    },
    primary: {
      main: "#131921",
      contrastText: "#fff",
    },
    secondary: {
      main: "#fb6a02",
      contrastText: "#fff",
    }
   }
})
  • 删除noSSr 修复了我的应用程序中的所有问题,包括客户端和服务器之间的样式不匹配。

我也在使用 NextJS + MUI v5,我 运行 在合并 Git b运行ches 后立即进入这个确切的错误。我怀疑合并损坏了缓存中的某些内容。我删除了 .next/ 的内容并重新启动了开发服务器,错误消失了。

我喜欢分享这个不匹配的案例:

next-dev.js?3515:32 Warning: Prop className did not match. Server: "MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-12 MuiSwitch-switchBase MuiSwitch-colorSecondary" Client: "MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-12 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-checked-13 Mui-checked"

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

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

问题是由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>,否则不渲染任何内容