mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.64k stars 32.22k forks source link

Server Rendering / SSR : CSS disappears upon js load, only in prod not dev #12894

Closed davalapar closed 6 years ago

davalapar commented 6 years ago

Expected Behavior

The css must not disappear when the js was loaded.

Current Behavior

The css disappears. This does not happen in webpack development, but only in production.

Steps to Reproduce

I was following the examples from the docs.

I noticed that the one from our docs here at https://material-ui.com/guides/server-rendering/ is different from the one from this github repo which is https://github.com/mui-org/material-ui/blob/master/docs/src/pages/guides/server-rendering/server-rendering.md, in which case I ended up following the latter since it solved some initial problems such as css name mismatches between the server & client. I also ensured that I've read the Troubleshooting part and didn't miss what's documented already.

The webpack.config.js I'm using is provided below, and I also tried commenting out the following parts related to splitChunks and runtimeChunk, but the css still disappears when the bundle js is loaded in the production build:

    // ...
    optimization: {
      /*
       * SplitChunks: {
       * chunks: 'all'
       * },
       * runtimeChunk: true,
       */
      minimize: Boolean(mode === 'production'),
      minimizer: [
        new UglifyJSWebpackPlugin({
          parallel: os.cpus().length,
          cache: true,
          uglifyOptions: {
            output: {
              comments: false
            },
            compress: {
              dead_code: true
            },
            mangle: true
          },
          sourceMap: true
        })
      ]
    },
    // ...

App.jsx:

import React from 'react';
import Button from '@material-ui/core/Button';
const App = () => (
  <Button variant="contained" color="primary" onClick={() => console.log('Clicked!')}>
    Hello World
  </Button>
);
export default App;

Client.jsx:

import '@babel/polyfill';
import React from 'react';
import { hydrate } from 'react-dom';
import {
  MuiThemeProvider,
  createMuiTheme,
  createGenerateClassName
} from '@material-ui/core/styles';
import green from '@material-ui/core/colors/green';
import red from '@material-ui/core/colors/red';
import JssProvider from 'react-jss/lib/JssProvider';
import App from './App';

class Main extends React.Component {
  componentDidMount () {
    const jssStyles = document.getElementById('jss-server-side');
    if (jssStyles && jssStyles.parentNode) {
      jssStyles.parentNode.removeChild(jssStyles);
    }
  }
  render () {
    return <App />;
  }
}
const theme = createMuiTheme({
  palette: {
    primary: green,
    accent: red,
    type: 'light'
  }
});
const generateClassName = createGenerateClassName({
  dangerouslyUseGlobalCSS: false
});
hydrate(
  (
    <JssProvider generateClassName={generateClassName}>
      <MuiThemeProvider theme={theme}>
        <Main />
      </MuiThemeProvider>
    </JssProvider>
  ),
  document.querySelector('#root'),
);

SSR.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 {
  MuiThemeProvider,
  createMuiTheme,
  createGenerateClassName
} from '@material-ui/core/styles';
import green from '@material-ui/core/colors/green';
import red from '@material-ui/core/colors/red';
import App from '../client/App';
const handleRender = (req, res) => {
  const sheetsRegistry = new SheetsRegistry();
  const sheetsManager = new global.Map();
  const theme = createMuiTheme({
    palette: {
      primary: green,
      accent: red,
      type: 'light'
    }
  });
  const generateClassName = createGenerateClassName({
    dangerouslyUseGlobalCSS: false
  });
  const html = renderToString(
    <JssProvider registry={sheetsRegistry} generateClassName={generateClassName}>
      <MuiThemeProvider theme={theme} sheetsManager={sheetsManager}>
        <App />
      </MuiThemeProvider>
    </JssProvider>
  );
  const css = sheetsRegistry.toString();
  const page = `
    <!doctype html>
    <html>
      <head>
        <title>Material-UI</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <style id="jss-server-side">${css}</style>
        <script src="/scripts/main.js" defer></script>
        <script src="/scripts/vendors~main.js" defer></script>
        <script src="/scripts/runtime~main.js" defer></script>
      </body>
    </html>
  `;
  res.send(page);
};
export default handleRender;
//...
import SSR from './SSR';
const app = express();
// ...
app.use(SSR);

webpack.config.js:

const os = require('os');
const path = require('path');
const webpack = require('webpack');
const UglifyJSWebpackPlugin = require('uglifyjs-webpack-plugin');
const webpackNodeExternals = require('webpack-node-externals');
const Client = (env, argv) => {
  const { mode } = argv;
  return {
    devtool: mode === 'development'
      ? 'source-map'
      : false,
    entry: [
      '@babel/polyfill',
      './src/client/Client.jsx'
    ],
    resolve: {
      extensions: [
        '.js',
        '.jsx'
      ]
    },
    module: {
      rules: [
        {
          enforce: 'pre',
          test: /\.(js|jsx)$/,
          use: 'eslint-loader',
          exclude: /node_modules/
        },
        {
          test: /\.worker\.js$/,
          use: [
            {
              loader: 'worker-loader',
              options: {
                name: '[name].js',
                publicPath: '/scripts/'
              }
            }
          ]
        },
        {
          test: /\.(js|jsx)$/,
          use: 'babel-loader',
          exclude: /node_modules/
        },
        {
          test: /\.(css)$/,
          use: [
            'style-loader',
            'css-loader'
          ]
        }
      ]
    },
    plugins: [
      new webpack.DefinePlugin({
        ENVIRONMENT: JSON.stringify(mode)
      })
    ],
    optimization: {
      splitChunks: {
        chunks: 'all'
      },
      runtimeChunk: true,
      minimize: Boolean(mode === 'production'),
      minimizer: [
        new UglifyJSWebpackPlugin({
          parallel: os.cpus().length,
          cache: true,
          uglifyOptions: {
            output: {
              comments: false
            },
            compress: {
              dead_code: true
            },
            mangle: true
          },
          sourceMap: true
        })
      ]
    },
    output: {
      path: path.join(__dirname, '/dist/client'),
      publicPath: '/scripts/'
    },
    stats: 'minimal'
  };
};
const Server = (env, argv) => {
  const { mode } = argv;
  return {
    devtool: mode === 'development'
      ? 'source-map'
      : false,
    entry: ['./src/server/Server.js'],
    resolve: {
      extensions: [
        '.js',
        '.jsx'
      ]
    },
    target: 'node',
    node: {
      __dirname: false,
      __filename: false
    },
    externals: [webpackNodeExternals()],
    module: {
      rules: [
        {
          enforce: 'pre',
          test: /\.(js|jsx)$/,
          use: 'eslint-loader',
          exclude: /node_modules/
        },
        {
          test: /\.(js|jsx)$/,
          use: 'babel-loader',
          exclude: /node_modules/
        }
      ]
    },
    plugins: [
      new webpack.DefinePlugin({
        ENVIRONMENT: JSON.stringify(mode),
      })
    ],
    output: {
      path: path.join(__dirname, '/dist/server')
    }
  };
};
module.exports = [
  Client,
  Server
];

Context

Nothing much special just App.jsx with a button.

When the bundle js is loaded, the components work great. Their styling just disappears, that's the problem.

(preparing the reproduction repo right now!)

Your Environment

Tech Version
Material-UI v3.0.3
React 16.5.1
Browser Chrome x64 Latest, Windows
davalapar commented 6 years ago

Reproduction repo: https://github.com/davalapar/template

evik42 commented 6 years ago

When you build the client with production settings, you need to run the server also with NODE_ENV='production' as it changes the generated class names.

davalapar commented 6 years ago

Thanks for responding, will look into it.

davalapar commented 6 years ago

Hi @evik42, I tried running it with that var set but I'm still getting the same disappearing css.

NODE_ENV=production node dist/server/main.js

$ NODE_ENV=production node dist/server/main.js
Warning: connect.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and will not scale past a single process.
Listening @ localhost:80
Listening @ localhost:443

Same NODE_ENV setting approach at https://github.com/mui-org/material-ui/commit/956e59e7fec2466596e7f3238fc7332ea99bfa74


Update:

oliviertassinari commented 6 years ago

@evik42's analysis is correct. You need to make the server render in production mode. NODE_ENV=production yarn run nodemon to the trick.

davalapar commented 6 years ago

@oliviertassinari thanks for responding. I tried it but unfortunately the css still disappears once the js files are loaded.

Screenshot here: https://i.imgur.com/BIjA7mk.png

Tried it with changes in webpack.config.js too :

I also tested the generateClassName to ensure they're working on server and client side and they're indeed producing matching name of classes.

Same result of css disappearing when js loads, while the component's onClick work without problems.. Could there be anything else I've overlooked here?

oliviertassinari commented 6 years ago

It was working all fine on my side with your reproduction. webpack has nothing to do with it. It's about using the same generator on the client and the sever (same options, same node env, same version).

speedy250 commented 5 years ago

Adding NODE_ENV=production to my server script has fixed the issue for me. Script command is now NODE_ENV=production run-s build start-prod