Zied Hamdi
Zied Hamdi

Reputation: 2662

Server-side rendering broken after upgrading to Material UI 5 (with Next.js)

I migrated my server-side rendering (SSR) working app to version 5 of MUI. I followed the official procedure, but when I disabled JavaScript, I received a raw HTML page (without CSS). You can see it here (sorry if it's down; I’m redeploying often to test).

  1. run codemod (v5.0.0/preset-safe and link-underline-hover)
  2. changed the makeStyles calls to use tss-react
  3. modified my _app and _document files according to the MUI example with Next.js

I launched the official SSR Next.js implementation. It showed it doesn't work either.

Server-side rendering with JavaScript disabled

For more details, here are the key files in my project:

_app.js

import * as React from 'react';
import Head from 'next/head';
import {ThemeProvider} from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import {CacheProvider} from '@emotion/react';
import theme from '../components/theme';
import createEmotionCache from "../lib/createEmotionCache";
import {StyledEngineProvider} from '@mui/material/styles';
import {ApolloProvider} from "@apollo/client";
import {SessionProvider} from "next-auth/react"
import {appWithTranslation} from "next-i18next";
import {useApollo} from "../apollo/client";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

//followed example: https://github.com/mui/material-ui/tree/master/examples/nextjs
function App(props) {
    const {Component, emotionCache = clientSideEmotionCache, pageProps} = props;
    const apolloClient = useApollo(pageProps)

    return (
        <CacheProvider value={emotionCache}>
            <StyledEngineProvider injectFirst>
                <ApolloProvider client={apolloClient}>
                    <SessionProvider session={pageProps.session}>
                        <Head>
                            <meta name="viewport" content="initial-scale=1, width=device-width"/>

                            <title>WeAlly</title>
                            <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
                            <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png"/>
                            <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png"/>
                            <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png"/>
                            <link rel="manifest" href="/images/site.webmanifest"/>
                        </Head>

                        <ThemeProvider theme={theme}>
                            {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
                            <CssBaseline/>
                            <Component {...pageProps} />
                        </ThemeProvider>
                    </SessionProvider>
                </ApolloProvider>
            </StyledEngineProvider>
        </CacheProvider>
    );
}


export default appWithTranslation(App);

_document.js

import * as React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import theme from '../components/theme';
import createEmotionCache from "../lib/createEmotionCache";

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang="en">
                <Head>
                    {/* PWA primary color */}
                    <meta name="theme-color" content={theme.palette.primary.main} />
                    <link rel="shortcut icon" href="/static/favicon.ico" />
                    <link
                        rel="stylesheet"
                        href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
                    />
                    <link rel="preconnect" href="https://fonts.googleapis.com"/>
                    <link rel="preconnect" href="https://fonts.gstatic.com"/>
                    <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;700&display=swap"
                          rel="stylesheet"/>

                    <link
                        href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;1,400&display=swap"
                        rel="stylesheet"/>

                    <link rel="stylesheet"
                          href="https://fonts.googleapis.com/icon?family=Material+Icons&display=swap"
                          media="screen"/>
                    {/* Inject MUI styles first to match with the prepend: true configuration. */}
                    {this.props.emotionStyleTags}
                </Head>
                <body>
                <Main />
                <NextScript />
                </body>
            </Html>
        );
    }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
    const originalRenderPage = ctx.renderPage;

    // You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
    // However, be aware that it can have global side effects.
    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) =>
                (function EnhanceApp(props) {
                    // console.log( 'enhancing app with cache: ', cache )
                    return <App emotionCache={cache} {...props} />;
                }),
        });

    const initialProps = await Document.getInitialProps(ctx);
    // This is important. It prevents emotion from rendering invalid HTML.
    // See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153
    const emotionStyles = extractCriticalToChunks(initialProps.html);
    // console.log('emotion style count: ', emotionStyles.styles.length)
    const emotionStyleTags = emotionStyles.styles.map((style) => (
        <style
            data-emotion={`${style.key} ${style.ids.join(' ')}`}
            key={style.key}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: style.css }}
        />
    ));

    return {
        ...initialProps,
        emotionStyleTags,
    };
};

**package.json**

{
  "name": "we-ally-org",
  "version": "1.0.0",
  "scripts": {
    "dev": "next",
    "back": "node ./server/starter.js",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@apollo/client": "^3.4.17",
    "@emotion/react": "^11.8.2",
    "@emotion/styled": "^11.8.1",
    "@google/maps": "^1.1.3",
    "@hapi/iron": "6.0.0",
    "@mui/icons-material": "^5.5.1",
    "@mui/material": "^5.5.2",
    "@mui/styles": "^5.5.1",
    "@next-auth/mongodb-adapter": "^1.0.3",
    "@next/bundle-analyzer": "^11.1.0",
    "@prisma/client": "2.16.1",
    "apollo-server-micro": "^3.5.0",
    "axios": "^0.21.1",
    "body-parser": "^1.19.0",
    "classnames": "^2.3.1",
    "cookie": "^0.4.1",
    "cors": "^2.8.5",
    "deepmerge": "4.2.2",
    "ejs": "^3.1.6",
    "express-graphql": "^0.12.0",
    "express-jwt": "^6.0.0",
    "express-session": "^1.17.2",
    "google-map-react": "^2.1.9",
    "graphql": "^15.5.1",
    "graphql-tools": "^8.1.0",
    "graphql-ws": "^5.4.0",
    "http-proxy": "^1.18.1",
    "image-type": "^4.1.0",
    "jodit-react": "^1.1.1",
    "jsonwebtoken": "^8.5.1",
    "linkify-react": "^3.0.4",
    "linkifyjs": "^3.0.5",
    "lodash": "^4.17.21",
    "micro": "^9.3.4",
    "moment": "^2.29.1",
    "mongodb": "^4.4.1",
    "next": "12",
    "next-auth": "^4.3.1",
    "next-compose-plugins": "^2.2.1",
    "next-i18next": "^8.5.1",
    "node-fetch": "^3.0.0",
    "passport": "^0.4.1",
    "passport-facebook": "^3.0.0",
    "pino": "^6.11.3",
    "prop-types": "^15.6.2",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-ga": "^3.3.0",
    "react-image-gallery": "^1.2.7",
    "react-moment": "^1.1.1",
    "react-player": "^2.9.0",
    "react-share": "^4.4.0",
    "react-use": "^17.2.4",
    "sanitize-html": "^2.4.0",
    "subscriptions-transport-ws": "^0.9.19",
    "tss-react": "^3.6.0"
  },
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.15.5",
    "@emotion/server": "^11.4.0",
    "babel-plugin-styled-components": "^2.0.6",
    "eslint": "8.11.0",
    "eslint-config-next": "12.1.0"
  }
}

Also, a dependency library was broken: react-image-gallery displays incorrectly since the upgrade, but I still didn't dig into that.

Upvotes: 5

Views: 7985

Answers (4)

Zabih arab
Zabih arab

Reputation: 51

I solve this problem with a loading splash screen when the splash screen is destroyed everything is ok and styles are entirely loaded an article about splash screen in nextjs

Upvotes: 1

Joseph Garrone
Joseph Garrone

Reputation: 1792

It was a Next.js issue that have been resolved since 12.1.7-canary.4 and up.

Upvotes: 1

rottitime
rottitime

Reputation: 2471

Issue for me was React 18. Reverted back to React 17 in package.json \

  "dependencies": {
        "react": "17.0.2",
        "react-dom": "17.0.2"

Upvotes: 2

Guilherme Felipe Reis
Guilherme Felipe Reis

Reputation: 918

Today, I have been working on this same issue on my side and I found the solution. You should remove your StyledEngineProvider from the app and modify your emotion Cache to:

 import createCache from '@emotion/cache';

// prepend: true moves MUI styles to the top of the <head> so they're loaded first.
// It allows developers to easily override MUI styles with other styling solutions, like CSS modules.
export const createEmotionCache = () =>
  createCache({ key: 'css', prepend: true });

I didn't understand what StyledEngineProvider was doing before, but it is changing the CSS order and adding the MUI styles to the top of the header, but if you add prepend=true in your cache solution you won't need this anymore. See this link.

Upvotes: 0

Related Questions