React Material-UI

Configuring React, Material-UI and Sass

React Material-UI

by John Vincent


Posted on May 27, 2018


This article describes how to configure React with Material-UI and Sass.

General

Material-UI

Installation

I am working with the next version of Material-UI

Install Material-UI

npm install --save material-ui@next
npm install --save material-ui-icons@next

or, more specifically

npm install --save material-ui@1.0.0-beta.37
npm install --save material-ui-icons@1.0.0-beta.36

Material-UI encourages the use of some non-standard language features. Install these

npm install babel-plugin-transform-class-properties --save-dev
npm install babel-plugin-transform-object-rest-spread --save-dev

Update .babelrc

{
"presets": ["env", "react"],
"plugins": ["transform-object-rest-spread", "transform-class-properties"]
}

Update .eslintrc

{
	"extends": ["airbnb", "prettier"],
	"env": {
		"browser": true
	},
	"parser": "babel-eslint",
	"globals": {},
	"rules": {
		"no-console": 0,
		"max-len": [
			"error",
			{
				"code": 100,
				"tabWidth": 2,
				"comments": 100,
				"ignoreTrailingComments": true,
				"ignoreUrls": true,
				"ignorePattern": "^import\\s.+\\sfrom\\s.+;$"
			}
		],
		"indent": [2, "tab", { "SwitchCase": 1 }],
		"no-tabs": 0,
		"react/jsx-indent": ["off", 2],
		"react/jsx-indent-props": ["off", 2],
		"jsx-a11y/anchor-is-valid": [
			"error",
			{
				"components": ["Link"],
				"specialLink": ["to"]
			}
		]
	},
	"plugins": ["react", "jsx-a11y", "import"]
}

The advanced language features required the use of the babel-eslint parser.

npm install babel-eslint --save-dev

Update webpack.config.js

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

const APP_FOLDER = path.resolve(__dirname, './src');
const SCSS_FOLDER = path.resolve(__dirname, './scss');
const ASSETS_FOLDER = path.resolve(__dirname, './src/assets');
const DIST_FOLDER = path.resolve(APP_FOLDER, './dist');
// const DIST_FOLDER_STYLE = path.resolve(DIST_FOLDER, './css');

const INCLUDE_SCSS_FOLDER = path.resolve(__dirname, './src');

const CopyWebpackPlugin = require('copy-webpack-plugin');

const ExtractTextPlugin = require('extract-text-webpack-plugin');

const extractSCSSBundle = new ExtractTextPlugin({
	filename: '[name].bundle.css',
	allChunks: true
});

require('dotenv').config(); // load from .env file

const config = {
	// entry: ['./src/index.jsx', './src/scss/index.scss', './src/components/main.scss'],
	entry: ['./src/index.jsx', './scss/index.scss'],

	output: {
		path: DIST_FOLDER,
		filename: 'bundle.js'
	},

	devtool: 'inline-source-map', // development
	// devtool: 'eval-source-map',		// development
	//	devtool: 'source-map',	// production
	devServer: {
		contentBase: DIST_FOLDER,
		compress: false, // true
		// inline: true,
		port: 8045,
		clientLogLevel: 'info',
		proxy: {
			'/api/**': { target: 'http://localhost:3001', changeOrigin: true, secure: false }
		}
	},

	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: /node_modules/,
				loader: 'babel-loader'
			},
			{
				test: /\.jsx$/,
				exclude: /node_modules/,
				loader: 'babel-loader'
			},
			// {
			// 	// regular css files
			// 	test: /\.css$/,
			// 	loader: extractCSS.extract({
			// 		fallback: 'style-loader/url!file-loader',
			// 		use: ['css-loader'],
			// 		publicPath: DIST_FOLDER_STYLE
			// 	})
			// },

			{
				test: /\.(sass|scss)$/,
				include: INCLUDE_SCSS_FOLDER,
				exclude: [SCSS_FOLDER, /node_modules/],
				use: ['style-loader', 'css-loader', 'sass-loader']
			},

			{
				test: /\.(sass|scss)$/,
				include: SCSS_FOLDER,
				exclude: [INCLUDE_SCSS_FOLDER, /node_modules/],
				loader: extractSCSSBundle.extract(['css-loader', 'sass-loader'])
			},
			{
				test: /\.(png|jpg|jpeg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
				include: ASSETS_FOLDER,
				loader: 'file-loader?name=assets/[name].[ext]'
			}
		]
	},
	resolve: {
		extensions: ['.js', '.jsx']
	},
	plugins: [
		new webpack.EnvironmentPlugin(['NODE_ENV', 'API_KEY']),
		extractSCSSBundle,
		new CopyWebpackPlugin([{ from: 'index.html', to: '.' }], { debug: 'info' })
	]
};

module.exports = config;

Notice there are 2 Sass sections.

  • The Sass code that is to bundled into styles.css is in ./scss. This code is compiled, loaded and bundled using extractSCSSBundle
  • The Sass code that is to be injected as styles into a React component is in the ./src folder. The Sass code is expected to be alongside the component that imports it. This code is compiled, loaded and injected with style-loader into the JavaScript.

Basic Configuration

The usual entry point, index.jsx

import React from 'react';
import ReactDOM from 'react-dom';

import { HashRouter as Router } from 'react-router-dom';

import { MuiThemeProvider } from 'material-ui/styles';

import { baseTheme } from './themes/theme';

import configureStore from './store/configureStore';
import Root from './containers/Root';

const store = configureStore();

document.addEventListener('DOMContentLoaded', () => {
	ReactDOM.render(
		<MuiThemeProvider theme={baseTheme}>
			<Router>
				<Root store={store} />
			</Router>
		</MuiThemeProvider>,
		document.getElementById('root')
	);
});

Uses baseTheme

import { createMuiTheme } from 'material-ui/styles';

import purple from 'material-ui/colors/purple';
import blue from 'material-ui/colors/blue';
import cyan from 'material-ui/colors/cyan';

export const baseTheme = createMuiTheme({
	palette: {
		primary: { main: blue[500] },
		secondary: { main: cyan[500] }
	},
	typography: {
...
	}
});

Sass Injection

Component scss

import React from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import { createMuiTheme } from 'material-ui/styles';
import Button from 'material-ui/Button';

import './scss.scss';

const theme = createMuiTheme({
	palette: {
		primary: {
			light: '#757ce8',
			main: '#3f50b5',
			dark: '#002884',
			contrastText: '#fff'
		},
		secondary: {
			light: '#ff7961',
			main: '#f44336',
			dark: '#ba000d',
			contrastText: '#000'
		}
	}
});

const Scss = () => (
	<MuiThemeProvider theme={theme}>
		<Button color="primary">Primary Button</Button>
		<Button color="secondary">Secondary Button</Button>
		<br />
		<Button classes={ { root: 'main' } }>button, classes root: main, injected</Button>
		<Button classes={ { root: 'jv2' } }>button, classes root: jv2, injected</Button>
		<div className="alphabetagamma">div, class alphabetagamma, from bundle</div>
		<div className="delta">div, class delta, from bundle</div>
		<div className="main">div, class main, injected</div>
	</MuiThemeProvider>
);

export default Scss;

The Sass to inject, ./scss.scss

.main {
  background-color: red !important;
  width: 150px;
  height: 160px;
}

.jv2 {
  background-color: brown !important;
  width: 250px;
  height: 260px;
}

Notice the use of !important to override the Material-UI choice of color.

Breaking this down.

<MuiThemeProvider theme={theme}>

loaded the theme.

<Button color="primary">Primary Button</Button>
<Button color="secondary">Secondary Button</Button>

use this theme.

<Button classes={ { root: 'main' } }>button, classes root: main, injected</Button>
<Button classes={ { root: 'jv2' } }>button, classes root: jv2, injected</Button>

instructs the Material-UI Button component to use classes defined in scss.scss

<div className="main">div, class main, injected</div>

instructs the div to use class "main", which is defined in scss.scss

<div className="alphabetagamma">div, class alphabetagamma, from bundle</div>
<div className="delta">div, class delta, from bundle</div>

refer to classes that are defined in the scss bundle, for example

index.scss

@import 'test';

_test.scss

.alphabetagamma {
  width: 300px;
  height: 300px;
  background-color: green;
}

.delta {
  width: 200px;
  height: 200px;
  background-color: yellow;
}