mui / material-ui

Material UI: Ready-to-use foundational React components, free forever. It includes Material UI, which implements Google's Material Design.
https://mui.com/material-ui/
MIT License
92.52k stars 31.89k forks source link

Class name hydration mismatch in server and client, (Warning: Prop `className` did not match Server and Client) #12203

Closed amerllica closed 5 years ago

amerllica commented 5 years ago

I don't know my issue is a bug or just a support term or just a configuration mismach, but I spend much time, Not happy go lucky writing here to somebody settle my issue. no!

I searched for 3 days, Even just sleep about 10 hours in this 3 days, try many ways, even I use Gitter but no one answer me, I see issues #10982 and #11506 and many Stack Overflow question and answers but I couldn't fix my issue and didn't find right way.

Absolutely I read and read many times the Material-UI Docs but I exactly did what the Docs said.

At last, I see class name hydration mismatch in server and client, 😞 this warning:

Warning: Prop `className` did not match. Server: "MuiFormLabel-root-134 MuiInputLabel-root-129 MuiInputLabel-formControl-130 MuiInputLabel-animated-133" Client: "MuiFormLabel-root-10 MuiInputLabel-root-5 MuiInputLabel-formControl-6 MuiInputLabel-animated-9"

I swear I tried many ways and searched a lot. I'm using Material-UI version 1.2.1.

This is my Index component as a root component:

    import React, {Component} from 'react';
    import Helmet from 'react-helmet';
    import styles from 'StylesRoot/styles.pcss';
    import TextField from '@material-ui/core/TextField';

    export default class App extends Component {

        render() {
            return (
                <div className={styles['root-wrapper']}>
                    <Helmet
                        htmlAttributes={{lang: 'fa', amp: undefined}}
                        bodyAttributes={{dir: 'rtl'}}
                        titleTemplate='اسکن - %s'
                        titleAttributes={{itemprop: 'name', lang: 'fa'}}
                        meta={[
                            {name: 'description', content: 'صفحه اتصال اعضاء'},
                            {name: 'viewport', content: 'width=device-width, initial-scale=1'},
                        ]}
                        link={[{rel: 'stylesheet', href: '/dist/styles.css'}]}
                    />
                    <TextField label='test' helperText='help'/>
                </div>
            );
        };
    }

Here below you can see my server.jsx and client.jsx:

    //--------------------------------------------client.jsx
    import React from 'react';
    import {hydrate} from 'react-dom';
    import {BrowserRouter} from 'react-router-dom';
    import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
    import {lightBlue, red} from '@material-ui/core/colors';
    import Index from './app/index';
    import RTL from './app/public/rtl';

    const theme = createMuiTheme({
        palette: {
            primary: lightBlue,
            accent: red,
            type: 'light',
        },
        direction: 'rtl',
    });

    class Main extends React.Component {
        // Remove the server-side injected CSS.
        componentDidMount() {
            const jssStyles = document.getElementById('jss-server-side');
            if (jssStyles && jssStyles.parentNode) {
                jssStyles.parentNode.removeChild(jssStyles);
            }
        }

        render() {
            return (
                <BrowserRouter>
                    <Index {...this.props}/>
                </BrowserRouter>
            );
        }
    }

    hydrate((
        <RTL>
            <MuiThemeProvider theme={theme}>
                <Main/>
            </MuiThemeProvider>
        </RTL>
    ), document.getElementById('root'));

    //--------------------------------------------server.jsx
    import React from 'react';
    import {renderToString} from 'react-dom/server';
    import {SheetsRegistry} from 'react-jss/lib/jss';
    import JssProvider from 'react-jss/lib/JssProvider';
    import {StaticRouter} from 'react-router-dom';
    import {Helmet} from "react-helmet";
    import {MuiThemeProvider, createMuiTheme, createGenerateClassName} from '@material-ui/core/styles';
    import {red, lightBlue} from '@material-ui/core/colors';
    import Template from './app/template';
    import Index from './app/index';
    import RTL from './app/public/rtl';

    export default function serverRenderer({clientStats, serverStats}) {
        return (req, res, next) => {
            const context = {};
            const sheetsRegistry = new SheetsRegistry();
            const theme = createMuiTheme({
                palette: {
                    primary: lightBlue,
                    accent: red,
                    type: 'light',
                },
                direction: 'rtl',
            });
            const generateClassName = createGenerateClassName();
            const markup = renderToString(
                <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}>
                    <RTL>
                        <MuiThemeProvider theme={theme} sheetsManager={new Map()}>
                            <StaticRouter location={req.url} context={context}>
                                <Index/>
                            </StaticRouter>
                        </MuiThemeProvider>
                    </RTL>
                </JssProvider>
            );
            const helmet = Helmet.renderStatic();

            const jss = sheetsRegistry.toString();

            res.status(200).send(Template({
                markup: markup,
                helmet: helmet,
                jss: jss,
            }));
        };
    }

So now it's needed to template.jsx:

    export default ({ markup, helmet, jss }) => {
        return `<!DOCTYPE html>
                <html ${helmet.htmlAttributes.toString()}>
                    <head>
                        ${helmet.title.toString()}
                        ${helmet.meta.toString()}                   
                        ${helmet.link.toString()}
                        <style id='jss-server-side'>${jss}</style>                                  
                    </head>
                    <body ${helmet.bodyAttributes.toString()}>
                        <div id='root'>${markup}</div>
                        <script src='/dist/client.js' async></script>
                    </body>
                </html>`;
    };

Ok, Now it is possible that this question is asked What is RTL? and Why you wrap MuiThemeProvider inside it in both server and client?

So first see RTL component:

    import React from 'react';
    import {create} from 'jss';
    import rtl from 'jss-rtl';
    import JssProvider from 'react-jss/lib/JssProvider';
    import {createGenerateClassName, jssPreset} from '@material-ui/core/styles';

    const jss = create({plugins: [...jssPreset().plugins, rtl()]});

    const generateClassName = createGenerateClassName();

    export default props => (
        <JssProvider jss={jss} generateClassName={generateClassName}>
            {props.children}
        </JssProvider>
    );

I saw it in Material-UI Docs and I believe the documentation is a little poor and could be improved. I asked myself, Why documentation pass a props.children for this function? and I guess maybe this function should wrap something. so I try many shapes and it works. but in the first call in browser after build. when I refresh the browser then the warning appears 😞 and I see this damn shape:

image

Surely I wanna see below shape, but I see it just once after build:

image

I don't know what is going wrong. I put this question on Stack Overflow too. And upload a mini repo for this issue, for seeing my issue just pull, and run npm install then num run dev. the project is accessible on localhost:4000

If I break some of your rules please forgive me but guide me how I can settle this issue.

basicfu commented 5 years ago

I have the same problem

oliviertassinari commented 5 years ago

@amerllica Reviewing the information provided, the root of your issue isn't with right-to-left but with server-side rendering on its own. I would encourage you to simplify the problem as much as possible: for instance, removing the right-to-left handling in the app to better understand what's going on. You most likely fall into this part: https://material-ui.com/guides/server-rendering/#css-works-on-only-on-first-load.

@basicfu For people who just want to get it working without addressing the root issue, we have a flag for that: https://material-ui.com/customization/css-in-js/#global-css

amerllica commented 5 years ago

Dear @oliviertassinari , thanks for answer, Maybe you don't believe me, But I did everything I guessed, I know someone professional like you, maybe think I just leave a question to get my solution but I try everything. Without any exaggeration, EVERYTHING.

When I omit <RTL> from my project the issue disappear completely. And every expectations are met, All UI expectations are met except right to left shapes.

Because of this I guess my issue is right to left configuration.

I remove RTL from my server.jsx and client.jsx too. so my server and client be like below:

//-------------------------------------------------client.jsx
import React from 'react';
import {hydrate} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
import {lightBlue, red} from '@material-ui/core/colors';
import Index from './app/index';

const theme = createMuiTheme({
    palette: {
        primary: lightBlue,
        accent: red,
        type: 'light',
    },
    direction: 'rtl',
});

class Main extends React.Component {
    // Remove the server-side injected CSS.
    componentDidMount() {
        const jssStyles = document.getElementById('jss-server-side');
        if (jssStyles && jssStyles.parentNode) {
            jssStyles.parentNode.removeChild(jssStyles);
        }
    }

    render() {
        return (
            <BrowserRouter>
                <Index {...this.props}/>
            </BrowserRouter>
        );
    }
}

hydrate((
    <MuiThemeProvider theme={theme}>
        <Main/>
    </MuiThemeProvider>
), document.getElementById('root'));

//-------------------------------------------------server.jsx
import React from 'react';
import {renderToString} from 'react-dom/server';
import {SheetsRegistry} from 'react-jss/lib/jss';
import JssProvider from 'react-jss/lib/JssProvider';
import {StaticRouter} from 'react-router-dom';
import {Helmet} from "react-helmet";
import {MuiThemeProvider, createMuiTheme, createGenerateClassName} from '@material-ui/core/styles';
import {red, lightBlue} from '@material-ui/core/colors';
import Template from './app/template';
import Index from './app/index';

export default function serverRenderer({clientStats, serverStats}) {
    return (req, res, next) => {
        const context = {};
        const sheetsRegistry = new SheetsRegistry();
        const theme = createMuiTheme({
            palette: {
                primary: lightBlue,
                accent: red,
                type: 'light',
            },
            direction: 'rtl',
        });
        const generateClassName = createGenerateClassName();
        const markup = renderToString(
            <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}>
                <MuiThemeProvider theme={theme} sheetsManager={new Map()}>
                    <StaticRouter location={req.url} context={context}>
                        <Index/>
                    </StaticRouter>
                </MuiThemeProvider>
            </JssProvider>
        );
        const helmet = Helmet.renderStatic();

        const jss = sheetsRegistry.toString();

        res.status(200).send(Template({
            markup: markup,
            helmet: helmet,
            jss: jss,
        }));
    };
}

Hence I do not wrap anything with RTL component. like my English projects, but for current project I have to settle right to left, If I don't, I should write right to left CSS for whole project and it kill my time.

I've no idea how to fix this. 😢

oliviertassinari commented 5 years ago

@amerllica Could you create a reproduction repository so I can have a look at it? Also, have you tried the dangerouslyUseGlobalCSS option?

amerllica commented 5 years ago

Dear @oliviertassinari , I saw several times this word, reproduction repository, I don't know what is it? or better explain, I don't know what you mean, If you mean I create a git repository and push a simple and small scale of my project in it, absolutely yes, I did it just like I said in my first post, I wrote it as mini repo, you can access to small and simple shape of my project that has the damn issue by this address.

And about dangerouslyUseGlobalCSS, I fear by its name, I've never used it.

oliviertassinari commented 5 years ago

@amerllica A "reproduction repository". "repository" is used in the git context. "reproduction" is used in this bug context. I have missed the link to https://gitlab.com/amerllica/varizHell. I will have a look at that, thank you.

oliviertassinari commented 5 years ago

You most likely fall into this part: https://material-ui.com/guides/server-rendering/#css-works-on-only-on-first-load.

Ok, I have found the root cause of your issue. It's definitely a configuration issue. I'm updating the documentation "Troubleshooting" section. I hope the new information will be enough to solve your problem. I don't want to give you the solution, I need to see if the new documentation I'm working on is efficient: it needs to scale.

oliviertassinari commented 5 years ago

And about dangerouslyUseGlobalCSS, I fear by its name, I've never used it.

No need to fear, it's simply a pattern I don't want to encourage but that people can still use. It's not that it's going to break, it just that I find it dangerous to rely on a global CSS scope for stuff like CSS overrides. But people have been doing it for a decade with libraries like Bootstrap, so it's fine.

amerllica commented 5 years ago

Thanks for your time and seeing my reproduction repository, I'm seeing Your Merge and I find:

const sheetsManager = new Map(); ...
<MuiThemeProvider theme={theme} sheetsManager={sheetsManager}> ...

I do just like your code but nothing change. The issue exists still. Also, I read the css-works-on-only-on-first-load Docs, But nothing changes after your merge.

I don't want to give you the solution, I need to see if the new documentation I'm working on is efficient: it needs to scale.

:point_up: :neutral_face:

I believe you act like a great teacher, but dude, at least please answer this question in Stack Overflow or guide me. Or tell me why it happens just in right-to-left configuration? Why it doesn't happen in normal mode?

oliviertassinari commented 5 years ago

@amerllica The relevant section for you is: https://github.com/mui-org/material-ui/blob/267beda0067f2d1dbefb4c90b9e724c9be50cbd6/docs/src/pages/guides/server-rendering/server-rendering.md#react-class-name-hydration-mismatch

amerllica commented 5 years ago

I've struggled too much, I slept 20 hours in these four days, Mayme my fatigue cause to this understanding. Or maybe <RTL> wrapping in both sides are not true way. :neutral_face:

I tried to vanish one of generateClassName in rtl.jsx and server.jsx or pass generateClassName as a props value to RTL component. But all of them Unfortunately All of my tries end up with the issue.

I'm confused. So confused. Definitely, the RTL Doc is not very complete. In this page just we can see a simple code, it is not very clear, Where it can be settled? Server? Client? Both? Could it be a separate component?

I Don't know :confused: I wanna go back home and sleep. :sleeping: :zzz:

amerllica commented 5 years ago

@oliviertassinari, I don't know this way is proper or not, Actually, I find out using separate RTL Component is not a good way, I use the Right-to-Left Documentation page for each server and client side separately, Hence server.jsx and client.jsx is like below:

//---------------------------------------------client.jsx
import React, {Component} from 'react';
import {hydrate} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import {
    MuiThemeProvider, createMuiTheme,
    createGenerateClassName, jssPreset
} from '@material-ui/core/styles';
import {create} from 'jss';
import rtl from 'jss-rtl';
import JssProvider from 'react-jss/lib/JssProvider';
import {lightBlue, red} from '@material-ui/core/colors';
import Index from './app/index';

const theme = createMuiTheme({
    palette: {
        primary: lightBlue,
        accent: red,
        type: 'light',
    },
    direction: 'rtl',
});

const jssRTL = create({plugins: [...jssPreset().plugins, rtl()]});

const generateClassName = createGenerateClassName();

class Main extends Component {
    componentDidMount() {
        const jssStyles = document.getElementById('jss-server-side');
        if (jssStyles) {
            jssStyles.remove();
        }
    }

    render() {
        return (
            <BrowserRouter>
                <Index {...this.props}/>
            </BrowserRouter>
        );
    }
}

hydrate((
    <JssProvider jss={jssRTL} generateClassName={generateClassName}>
        <MuiThemeProvider theme={theme}>
            <Main/>
        </MuiThemeProvider>
    </JssProvider>
), document.getElementById('root'));

//---------------------------------------------server.jsx
import React from 'react';
import {renderToString} from 'react-dom/server';
import {SheetsRegistry} from 'react-jss/lib/jss';
import JssProvider from 'react-jss/lib/JssProvider';
import {StaticRouter} from 'react-router-dom';
import {Helmet} from "react-helmet";
import {MuiThemeProvider, createMuiTheme, createGenerateClassName, jssPreset} from '@material-ui/core/styles';
import {create} from 'jss';
import rtl from 'jss-rtl';
import {red, lightBlue} from '@material-ui/core/colors';
import Template from './app/template';
import Index from './app/index';

export default function serverRenderer({clientStats, serverStats}) {
    return (req, res, next) => {

        const context = {};

        const sheetsRegistry = new SheetsRegistry();

        const theme = createMuiTheme({
            palette: {
                primary: lightBlue,
                accent: red,
                type: 'light',
            },
            direction: 'rtl',
        });

        const jssRTL = create({plugins: [...jssPreset().plugins, rtl()]});

        const generateClassName = createGenerateClassName();

        const markup = renderToString(
            <JssProvider jss={jssRTL} registry={sheetsRegistry} generateClassName={generateClassName}>
                <MuiThemeProvider theme={theme} sheetsManager={new Map()}>
                    <StaticRouter location={req.url} context={context}>
                        <Index pathname={req.url}/>
                    </StaticRouter>
                </MuiThemeProvider>
            </JssProvider>
        );
        const helmet = Helmet.renderStatic();

        const jss = sheetsRegistry.toString();

        res.status(200).send(Template({
            markup: markup,
            helmet: helmet,
            jss: jss,
        }));
    };
}

It works well on both sides, server and client and makes consistent Material-UI CSS in style tag.

nosykretts commented 5 years ago

I also have this problem. Using Next.js with Layout _app.js component

I have CustomButton component that called _app Layout (Header part) and also in the main content.

This is my repro case for this issue. https://github.com/nosykretts/mui-ssr-caching-problem.

Its from combination from two examples : direct clone https://github.com/mui-org/material-ui/tree/master/examples/nextjs and add some. https://github.com/zeit/next.js/tree/canary/examples/with-app-layout

I think its not from Material UI. Its from react-jss. This maybe relevant issue https://github.com/cssinjs/jss/issues/704

But I need help.

@oliviertassinari can you help me in my repo? Whats wrong with my setup. I search everything in MUI doc, jss doc, and react-jss repo. But no luck.

Thanks

lucasavila00 commented 5 years ago

I'm having the same issue and I don't know a lot to help fix it. Anyway, as I wait for the fix I have come up with a way to keep developing without broken styles. If you comment componentDidMount away from _app.js the visual issue will go away and it is possible to keep working on the UI. This is not a fix but just a way to keep developing while this issue is not fixed.

Thanks 👍

up209d commented 5 years ago

@nosykretts : I think the problem is not from next, jss or mui. Your wrapping strategy is problematic as you used MainLayout to wrap Index, I would not make MainLayout wrap the Index cuz

App > Index > Anything Content Component

So App is used for Router + Provider > Index is main case Page Component returned by Router > MainLayout is a Layout Component along with Header & Footer Components

_app.js

<MainLayout>
  <Component pageContext={this.pageContext} {...pageProps} />
</MainLayout> 

// Component here is supposed to be Index, so you need to leave it alone as
<Component pageContext={this.pageContext} {...pageProps} />  

index.js

  render() {
    const { classes } = this.props;
    return (
      <MainLayout className={classes.root}>
        <Typography variant="display1" gutterBottom>
          Material-UI
        </Typography>
        <TroubleButton />
      </MainLayout>
    );
  }
amerllica commented 5 years ago

@up209d , Thanks a lot, I guessed my wrapping cause to that issue, and when I omitted the wrapping and used the normal function in both sides everything became right, But thanks for your kind

nosykretts commented 5 years ago

@up209d Thanks for your help and your excellent explanation. It really work without error.

The problem with your solution is that the oldway next js do Layout Component. The oldway require Layout Component wrapped in every pages. Main disadvantages is when user switching page for example from HomePage to AboutPage. It will redownload Layout component again.

My MainLayout is Big. It contains Header, Drawer also Heavy footer. lots of texts. Its important to my app.

The new way Layout Component is layout component is supposed to imported and called inside _app.js. and I think this _app.js best new feature from NextJS.

This line is from official NextJS with-app-layout example

https://github.com/zeit/next.js/blob/0f4e9feafad9b616cde7276c6db204608be31900/examples/with-app-layout/pages/_app.js#L17

Thanks again.

up209d commented 5 years ago

@nosykretts In my understanding, your server-side actually renders markup content string from Index Component only as can be seen in _document.js

<Component pageContext={this.pageContext} {...pageProps} />

When you were trying to render jss classes from outside of the Index as there is one TroubleButton which is actually outside of Index, client-side will do that but not server-side, that's how the mismatch classes error returned. If you want the Header and Footer or Any Component are fixed in every page case, I suggest you can do the Router inside your Index Component. Or you can make server-side render markup from the whole App Component

valorloff commented 5 years ago

@oliviertassinari, In last docs (4.0) about SSR no any mention about createGenerateClassName and jssProvider, why? Should it be used createGenerateClassName on client/server apps? Where should I import it from, @material-ui/styles, @material-ui/core? Please give more doc info about latest changes in SSR, especially due of headache with React Apollo....

oliviertassinari commented 5 years ago

It's no longer needed. You can follow the new API.

valorloff commented 5 years ago

But how to fight className mismatch in Apollo's double traversing React tree without jssProvider? Make two copies of new ServerStyleSheets() with different options?

oliviertassinari commented 5 years ago

You don't need to handle the styles in the first Apollo rendering pass. Ideally, should disable the styles generation.

valorloff commented 5 years ago

if you mean const sheets = new ServerStyleSheets({disableGeneration:true}); then the page turns out to be totally without styles. Can you please show little example, what you mean?