1

I have a React project that is bundled by Webpack and served up by react_on_rails. In this project I use the Select component from react-select. Everything works as expected when using Webpack 3. After upgrading to Webpack 4, everything also works as expected in development mode. However, when I build in production mode, the Select component from react-select does not have any styling applied to it.

(I don't have enough reputation points to post images so I am going to provide links to the images.)

Here is what the selector looks like when built in development mode.

selector with styling

And here is what the selector looks like when built in production mode.

selector without styling

The reason the styles are not applied is that react-select uses Emotion css-in-js and the css gets injected into the head in stylesheets.

Here is an example in the head when in development mode.

screenshot of stylesheets in the head

These style tags are all absent in the head when in production mode.

I have narrowed it down to the fact that it seems to be caused by the webpack minification step. If I add

optimization: {
  minimize: false
}

to my webpack.config.js, then the styles are present when in production mode.

Here's my webpack.config.js without the optimization added:

const webpack = require('webpack');
const pathLib = require('path');

const devBuild = process.env.NODE_ENV !== 'production';

const config = {
  entry: [
    'es5-shim/es5-shim',
    'es5-shim/es5-sham',
    'babel-polyfill',
    './app/bundles/analytic',
    './app/bundles/Pulse/startup/registration',
  ],
  output: {
    filename: 'webpack-bundle.js',
    path: pathLib.resolve(__dirname, '../app/assets/webpack'),
  },
  devtool: "source-map",
  resolve: {
    extensions: [".ts", ".tsx", '.js', '.jsx'],
  },
  plugins: [
    new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
  ],
  module: {
    rules: [
      {
        test: /travel-info-type.ts/,
        use: [{
          loader: 'expose-loader',
          options: 'TravelInfoType'
        }]
      },
      {
        test: /heatmap-getter.ts/,
        use: [{
          loader: 'expose-loader',
          options: 'HeatmapGetter'
        }]
      },
      {
        test: /data-hub.ts/,
        use: [{
          loader: 'expose-loader',
          options: 'DataHub'
        }]
      },
      {
        test: /exported-functions.js/,
        use: [{
          loader: 'expose-loader',
          options: 'ExportedFunctions'
        }]
      },
      {
        test: /analyticsTracker.ts/,
        use: [{
          loader: 'expose-loader',
          options: 'analyticsTracker'
        }]
      },
      {
        test: /railsAnalytics.js/,
        use: [{
          loader: 'expose-loader',
          options: 'railsAnalytics'
        }]
      },
      {
        test: require.resolve('react'),
        use: {
          loader: 'imports-loader',
          options: {
            shim: 'es5-shim/es5-shim',
            sham: 'es5-shim/es5-sham',
          }
        },
      },
      {
        test: /\.(woff|woff2|eot|ttf|svg|gif|png)$/,
        use: [{
          loader: 'url-loader'
        }],
      },
      {
        test: /\.jsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
      // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
      { test: /\.tsx?$/, loader: "ts-loader" },

      // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
      { enforce: "pre", test: /\.js$/, loader: "source-map-loader" },
      // Extract css files
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.scss$/,
        use: [ "style-loader", "css-loader", "sass-loader" ],
      },
    ],
  },
};

module.exports = config;

if (devBuild) {
  console.log('Webpack dev build for Rails'); // eslint-disable-line no-console
  module.exports.devtool = 'eval-source-map';
} else {
  console.log('Webpack production build for Rails'); // eslint-disable-line no-console
}

And here is my package.json

{
  "name": "myProject",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "build:test": "webpack --config webpack.config.js",
    "build:production": "NODE_ENV=production webpack --mode=production --config webpack.config.js",
    "build:development": "webpack --mode=development -w --config webpack.config.js",
    "test": "jest",
    "test:watch": "yarn test --watch",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook -o ../public/storybook"
  },
  "cacheDirectories": [
    "node_modules",
    "client/node_modules"
  ],
  "dependencies": {
    "actioncable": "^5.2.0",
    "color-convert": "^1.9.0",
    "es5-shim": "^4.5.9",
    "expose-loader": "^0.7.3",
    "imports-loader": "^0.7.1",
    "js-cookie": "^2.2.0",
    "moment": "^2.18.1",
    "prop-types": "^15.5.7",
    "rc-slider": "^8.6.7",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-on-rails": "6.9.3",
    "react-onclickoutside": "^5.11.1",
    "react-redux": "^6.0.0",
    "react-router-dom": "^4.1.1",
    "react-select": "^2.3.0",
    "react-table": "^6.0.5",
    "react-toggle-switch": "^2.1.3",
    "react-tooltip": "^3.6.1",
    "redux": "^4.0.1",
    "redux-batched-actions": "^0.2.0",
    "redux-thunk": "^2.3.0",
    "rxjs": "5.5.2"
  },
  "devDependencies": {
    "@storybook/addon-knobs": "^3.4.11",
    "@storybook/addons": "^3.4.11",
    "@storybook/react": "^3.4.11",
    "@types/actioncable": "^0.0.2",
    "@types/bugsnag": "^2.5.28",
    "@types/google-maps": "^3.2.0",
    "@types/googlemaps": "^3.26.11",
    "@types/highcharts": "^4.2.55",
    "@types/jest": "23.3.10",
    "@types/jquery": "^2.0.45",
    "@types/js-cookie": "^2.2.0",
    "@types/lodash": "^4.14.118",
    "@types/moment": "^2.13.0",
    "@types/rc-slider": "^8.6.3",
    "@types/react": "^16.8.1",
    "@types/react-dates": "^16.0.5",
    "@types/react-dom": "16.0.11",
    "@types/react-redux": "^7.0.1",
    "@types/react-router": "^4.0.26",
    "@types/react-router-dom": "^4.2.7",
    "@types/react-select": "^2.0.11",
    "@types/react-tooltip": "^3.3.5",
    "ts-loader": "^5.3.3",
    "babel-cli": "^6.23.0",
    "babel-core": "^6.23.1",
    "babel-loader": "^7.1.5",
    "babel-polyfill": "^6.23.0",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-react": "^6.23.0",
    "babel-preset-stage-2": "^6.22.0",
    "babel-runtime": "^6.23.0",
    "css-loader": "^0.28.0",
    "enzyme": "^3.8.0",
    "enzyme-adapter-react-16": "^1.9.0",
    "highcharts": "^6.0.3",
    "jest": "23.3.0",
    "jquery": "^3.2.1",
    "jsdom": "^10.0.0",
    "node-sass": "^4.9.3",
    "react-test-renderer": "^16.7.0",
    "redux-mock-store": "^1.2.3",
    "sass-loader": "^7.1.0",
    "sinon": "^2.4.1",
    "source-map-loader": "^0.2.1",
    "storybook-addon-jsx": "^5.4.0",
    "style-loader": "^0.16.1",
    "ts-jest": "23.10.5",
    "typescript": "^3.0.1",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.5",
    "webpack-cli": "^3.2.3"
  }
}

And here is the component that is using the Select component:

import * as React from 'react'

import Select from 'react-select'

import { MultiSelectOption } from '../interfaces/SelectionUI'

class MultipleSelectPicker extends React.PureComponent<MultipleSelectPickerProps> {

  onChange = (allSelected: MultiSelectOption[]) => {
    const {
      onAdd,
      onRemove,
      values,
    } = this.props

    if (values.length < allSelected.length) {
      const addedOption = allSelected.find(selected => !values.includes(selected))

      onAdd(addedOption)
    }
    else if (values.length > allSelected.length) {
      const removedOption = values.find(value => !allSelected.includes(value))

      onRemove(removedOption)
    }
  }

  render() {

    const {
      name,
      values,
      options,
      placeholder,
    } = this.props

    return (
      <Select
        name={name}
        value={values}
        className={`${name} selectpicker`}
        options={options}
        onChange={this.onChange}
        isMulti
        placeholder={placeholder}
      />
    )
  }
}

export interface MultipleSelectPickerProps {
  name: string,
  options: MultiSelectOption[],
  values: MultiSelectOption[],
  placeholder?: string,
  onAdd: (addedOption: MultiSelectOption) => void,
  onRemove: (removedOption: MultiSelectOption) => void,
}

export default MultipleSelectPicker

Anyone have an idea about why the Webpack 4 minimization would keep the react-select Emotion stylesheets from getting injected and how to fix that?

1 Answers1

0

I found a workaround. I swapped out the default webpack minimizer for the (UglifyJsPlugin)[https://github.com/webpack-contrib/uglifyjs-webpack-plugin] and now everything works as expected.

  const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

  optimization: {
    minimizer: [ new UglifyJsPlugin() ],
  }