React

workflows

Without Create-React-App

First Edition

Created by Maria D. Campbell / @letsbsocial1

About This Presentation

This is the first edition of my React Workflows Without CRA. If you want to acquaint yourself with the second edition as well, please visit the React Workflow Updated 2018 repository. For a detailed map of this edition, please visit the README of this edition's repository.

Hi There!

I'm Maria.

About Me

I've always been obsessed with development and construction in one way or another.

Background

  • Fashion design and manufacture.
  • Graphics/production art.
  • Now Fullstack JS, with a focus on React.

?

So why am I here today to discuss React workflows?

Simple.

When I started learning React, I was not happy with the fact that virtually all React learning seemed to revolve around using create-react-app to create React applications.

Horse blinders

And that meant blindly accepting its extensive boiler-plate.

Why blindly? Because the workflow is hidden by default. In order to see what is going on, you have to eject out of CRA.

But once you eject, THERE IS NO GOING BACK!

And one thing I know for sure ...

Starting Over

Sometimes it's faster (and easier) to start from scratch than try to manipulate to your needs.

And that's where

webpack

(2 and beyond) came in.

?

So why webpack?

Grunt vs Gulp

If done from scratch, it can mean fewer workflow files and lines of code. If you come from task runners like Gulp (I do) or Grunt, you know what I'm talking about.

Create-React-App

And if you are familiar with CRA, you know how extensive its boilerplate is.

Nothing wrong with that

if your focus is on writing JSX and JS for the browser.

The Problem

with that approach is that when developers start heavily relying on tools like that, they can lose site of the bigger picture.

  • Of how things work.
  • Why they work as they do.
  • What their options are.

Which can STUNT one's growth as a developer.

🐜

I first got the bug to create my own custom React workflows when I created my first React application using CRA.

Debugging Difficulties

I would get errors in my React projects which I sometimes found difficult to debug. They might or might not have been CRA related.

Custom React Workflows

So I decided to start creating my own React workflows using webpack.

Module Bundler


						module.exports = {};
						

webpack is NOT a task runner. It is a module bundler.

Dependency Graph

When webpack processes your app, it recursively builds a dependency graph that includes every module that your app needs, and then packages all of those modules into a small number of bundles to be loaded by the browser.

webpack basics

To get started, you only need to understand 4 basic concepts:

  • Entry
  • Output
  • Loaders
  • Plugins

entry


				module.exports = {
				  entry: {
					bundle: './src/index.js',
					vendor: VENDOR_LIBS
				  },
				}
						

The starting point of webpack's dependency graph is called the entry point. The entry point tells webpack where to start its bundling process, and then follows the graph of dependencies to know what to bundle.

bundle


				module.exports = {
				  entry: {
					bundle: './src/index.js',
					vendor: VENDOR_LIBS
				  },
				}
						

webpack defines entry points using the entry property in the webpack configuration object, module.exports = {}. The value of the bundle property indicates to webpack what code needs to be bundled.

main index.js


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

				import './style/style.css';
				import 'font-awesome-webpack';

				import Container from './components/Container';

				import './images/favicon.ico';
						

The files which need to be included in the bundled file in output have all been imported into the main index.js file.

ReactDOM.render()

And the main App Component that holds everything else related to the app is rendered there as well.

vendor


				module.exports = {
				  entry: {
					bundle: './src/index.js',
					vendor: VENDOR_LIBS
				  },
				}
						

You may have noticed I add a second entry point called vendor.

VENDOR_LIBS


				const VENDOR_LIBS = [
				  'react', 'react-dom', 'prop-types'
				];
						

Relates to non-webpack, 3rd party plugins.

Code Splitting

is important for browser caching.

Browser Caching

is great because it allows sites to load faster once the client has grabbed the necessary files and assets from the server the first time a user visits the page.

Clearing The Cache

Caching Headaches

However, it can cause big headaches when new code has been added to files. If the name of a file remains the same when new code has been added to it, the browser thinks that nothing has changed. To the browser, same name means same content.

Output


				output: {
				  path: path.join(__dirname, 'dist'),
				  filename: '[name].[chunkhash].js'
				},
						

Once you've bundled all your assets together, you need to tell webpack where to bundle your app. The output object tells webpack what to do with bundled code.

path


				const path = require('path');
				output: {
				  path: path.join(__dirname, 'dist'),
				  filename: '[name].[chunkhash].js'
				},
						

The value of the first property, path, is the absolute path to the dist folder, where the bundled assets end up. It has to be required in your webpack.config.js in order for you to able to use it.

filename


				output: {
				  path: path.join(__dirname, 'dist'),
				  filename: '[name].[chunkhash].js'
				},
						

The value of the second property, filename, refers to the name(s) of output bundle(s).

name


				const path = require('path');
				output: {
				  path: path.join(__dirname, 'dist'),
				  filename: '[name].[chunkhash].js'
				},
						

I use the [name] property because of my code splitting resulting from two entry points. [name] takes into consideration that there can be more than one bundle file, and they may have different names.

[chunkhash]


				const path = require('path');
				output: {
				  path: path.join(__dirname, 'dist'),
				  filename: '[name].[chunkhash].js'
				},
						

By default, [chunkhash] places a new hash each time you run a new build. This is not ideal for browser caching. That could mean re-grabbing a file that has not changed!

expectations !== reality

I came to realize that simply adding [chunkhash] to my filename didn't exactly achieve what I thought it would out of the box.

webpack docs

After rummaging through the newly (and much) improved webpack documentation, I came across the section called Caching. It provided some of the answers I was seeking.

So why did the [chunkhash] for my bundle.js and vendor.js files change every time I ran a new build, even if no changes were made to them?

Because webpack contains certain boilerplate, specifically the runtime and manifest, on entry.

< /> === 3

There are 3 main types of code in a typical app or site built with webpack:

  • The source code you have written
  • Third party, vendor code your source is dependent on
  • Webpack runtime and manifest that conducts the interaction of all modules

Runtime

The runtime, along with the manifest data, is basically all the code webpack needs to connect your modularized app while it's running in the browser.

Manifest

manifest refers to the collection of module related data runtime uses to resolve and load modules after they have been bundled and shipped to the browser.

Side Effects

So why mention all of this? Because runtime and manifest affect browser caching.

[chunkhash]

When you start using [chunkhash], certain hashes change even when their content does not. This is caused by the injection of runtime and manifest, which change every build.

Commons Chunk Plugin

We can use the webpack CommonsChunkPlugin to extract webpack's boilerplate runtime and manifest.

[name] > 1


				new webpack.optimize.CommonsChunkPlugin({
				  name: 'vendor',
				  minChunks: Infinity
				}),
				new webpack.optimize.CommonsChunkPlugin({
				  name: 'runtime'
				}),
						

I added a second [name] property with the value of runtime so I could extract the runtime/manifest data code from vendor.js to create a separate runtime.js file.

Separating Concerns

Separating runtime code from vendor code is the first step towards making sure that the vendor hash does not change with each new build unless a new 3rd party plugin is added.

Resolving Order


				[gQNZ] ./src/style/style.css 307 bytes {1} [built]
				[lVK7] ./src/index.js 846 bytes {1} [built]
				[olkN] ./src/store.js 354 bytes {1} [built]
				[pnOm] ./src/App.js 3.01 kB {1} [built]
				[tuRH] ./src/reducers/todo.js 874 bytes {1} [built]
				[z7yQ] ./src/logo.svg 3.52 kB {1} [built]
				+ 2 hidden modules
						

However, this code splitting is not enough. Each time a new build takes place, each module.id is incremented based on resolving order by default. When the resolving order is changed, the module.id changes.

Blocking Change

So how can we prevent vendor's [chunkhash] from changing? There are two webpack plugins that can get us there. One is the NamedModulesPlugin, and the other is the HashedModuleIdsPlugin.

Named Modules Plugin


				new webpack.NamedModulesPlugin(),
				new webpack.optimize.CommonsChunkPlugin({
				  name: 'vendor',
				  minChunks: Infinity
				}),
						

Uses the path to the module instead of a numerical identifier. It's useful in development because the output is more readable, but it also takes a bit longer to run.

Hashed Module Ids Plugin


				new webpack.HashedModuleIdsPlugin(),
				new webpack.optimize.CommonsChunkPlugin({
				  name: 'vendor',
				  minChunks: Infinity
				}),
						

Recommended for production builds. It causes hashes to be based on the relative path of the module, generating a 4 character string as the module id.

Vendor remains constant

So if we were to run a new build, only the [chunkhash] for bundle.js and runtime.js would change.

Loaders

webpack loaders transform your files into modules as they are added to your dependency graph. Loaders have 2 main purposes:

  • Identify which file or files should be transformed by a loader (test property)
  • Transform those files so that they can be added to your dependency graph, and eventually your bundle.js. (use property)

JS? Rules


				rules: [
				  {
					test: /\.js?/,
					use: {
					  loader: 'babel-loader',
					  options: {
						presets: ['env', 'stage-1', 'stage-2', 'jest', 'react']
					  }
					},
					exclude: /node_modules/
				  },
				]
						

JS? test property


				test: /\.js?/,
						

The test property tells webpack what kind of file to match to the babel-loader and its preset options,

JS? use property


				use: {
				  loader: 'babel-loader',
				  options: {
					presets: ['env', 'stage-1', 'stage-2', 'jest', 'react']
				  }
				},
						
					

and then it uses that loader to transform those files before adding it to bundle.js. The regex /\.js?/ refers to any extension that starts with .js. This means that .jsx files could and would be included.

Exclude Property


				exclude: /node_modules/
						

The exclude property means that whatever modules match the /node_modules/ regex should be excluded from bundle.js.

CSS test property


				test: /\.css$/,
						

CSS use property


				{
				  test: /\.css$/,
				  use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
					fallback: 'style-loader',
					use: ['css-loader', 'postcss-loader']
				  })),
				},
						

css hot loader

Supports hot module replacement (HMR) for an extracted css file. shepherdwind, the creator of css-hot-loader, came up with the idea because style-loader, which also can achieve css hot reload, needs to inject a style tag into index.html. And this can happen before JS scripts are loaded, resuslting in a page with no styles.

css-hot-loader on Github extract-text-webpack-plugin issue #30 extract-text-webpack-plugin issue #89

Hot Module Replacement

HMR exchanges, adds, or removes modules while an app is running, without a full reload. It can speed up development because:

  • Can retain app state which is lost during a full reload.
  • Save valuable development time by only updating what has changed.
  • Tweak styling faster.

Extract Text Webpack Plugin


				new ExtractTextPlugin({
				  filename: 'style.[contenthash:8].css',
				}),
						

Extracts text from bundle.js into its own separate file. And the CSS bundle is loaded in parallel to the JS bundle, thereby speeding up load time.

[contenthash]

[contenthash] returns a hash specific to content. It is available for the extract-text-webpack-plugin only, and is the most specific hash option available in webpack. And it's great to use with css files for browser caching. The hash only changes when the css changes.

Image Webpack Loader


				import logo from './logo.svg';

				
logo

Welcome to React with Redux

Image loader module for webpack, and this is what it permits you to do in your React Component.

Image Webpack Loader


				{
				  test: /\.(jpeg?g|png|gif|svg|ico)$/,
				  use: [
					{
					  loader: 'url-loader',
					  options: { limit: 40000 }
					},
					'image-webpack-loader'
				  ]
				},
						

The code you need to add to your webpack config.

Plugins

Loaders only execute a transform on a per-file basis. Plugins are most commonly used to perform actions and custom functionality on "compilations" or "chunks" of your bundled modules.

Require


				const htmlWebpackPlugin = require('html-webpack-plugin');
				plugins: [

				]
						

In order to use a plugin, you have to require it,

Add


				plugins: [
				  new htmlWebpackPlugin({
					template: 'src/index.html',
					favicon: 'src/images/favicon.ico',
					inject: true
				  }),
				]
						

and then add it to the plugins array. And since you can use the same plugin many times in the same config for different purposes, you need to create an instance of it by calling it with new.

HTML Webpack Plugin


				plugins: [
				  new htmlWebpackPlugin({
					template: 'src/index.html',
					favicon: 'src/images/favicon.ico',
					inject: true
				  }),
				]
						

Simplifies the creation of your html files. Useful for webpack bundles that include hashes in the filenames. Great for html templating.

Named Modules Plugin


				new webpack.NamedModulesPlugin(),
				new webpack.optimize.CommonsChunkPlugin({
				  name: 'vendor',
				  minChunks: Infinity
				}),
						

Will cause the display of the relative path of a module in Terminal when HMR is enabled. Good for development.

Hashed Module Ids Plugin


				new webpack.HashedModuleIdsPlugin(),
				new webpack.optimize.CommonsChunkPlugin({
				  name: 'vendor',
				  minChunks: Infinity
				}),
						

Will cause hashes to be based on the relative path of a module in Terminal, generating a 4 character string as the module id. Good for production.

Commons Chunk Plugin


				new webpack.optimize.CommonsChunkPlugin({
				  name: 'vendor',
				  minChunks: Infinity
				}),
				new webpack.optimize.CommonsChunkPlugin({
				  name: 'runtime'
				}),
						

Creates a separate file known as a chunk, consisting of common modules shared between multiple entry points.

Benefits

By separating common modules from bundles, the resulting chunked file can be loaded once initially, and stored in cache for later use. This results in pagespeed optimization because the browser can serve the shared code from the cache instead of loading a larger bundle whenever a new page is visited.

Extract Text Webpack Plugin


				new ExtractTextPlugin({
				  filename: 'style.[contenthash:8].css',
				}),
						

We use the extract-text-webpack-plugin with our css loaders to execute the initial extraction of css from bundle.js. But this css also needs to be told WHERE to go.

Define Plugin


				new webpack.DefinePlugin({
				  DEV: true
				}),
						

Allows you to create global constants which can be configured at compile time.

Benefit


				new webpack.DefinePlugin({
				  DEV: false,
				  'process.env.NODE_ENV': JSON.stringify('production')
				}),
						

Allows for different behavior between development and production builds.

devtool property


				devtool: 'eval-source-map',
						

Controls how source maps are generated. With eval-source-map, each module is executed with eval() and a SourceMap is add as a DataUrl to the eval(). Initally slow, but fast on rebuild and yields real files. Lines are also correctly mapped because they get mapped to the original code. Good for development.

devtool property


				devtool: 'source-map',
						

With source-map, a full SourceMap is emitted as a separate file. It adds a reference comment to the bundle so devtools knows where to find it. Good for production. Other production options are none, hidden-source-map, and nosources-source-map.

devServer Object


				devServer: {
				  inline: true,
				  stats: 'minimal',
				  open: true,
				  contentBase: './src/',
				  historyApiFallback: true,
				  port: port
				},
						

Picked up by the webpack-dev-server plugin and can be used to change its behavior in various ways.

inline property


				inline: true,
						

Means that a script will be inserted in your bundle to take care of live reloading, and build messages will appear in the terminal console.

stats property


				stats: 'minimal',
						

The stats property refers to the stats that are printed out to the Terminal console whenever you either run the webpack-dev-server for development or run a new production build. For development, it is best to print out stats highlights rather than more detailed information.

open property


				open: true,
						

Means that the webpack-dev-server will open your default browser when it launches.

contentBase property


				contentBase: './src/',
						

Tells the server where to serve content from. Only necessary if you want to serve static files. And of course we do! index.html.

ContentBase message


				webpack output is served from /
				Content not from webpack is served from ./src/
						

With contentBase set to ./src/, the message pertaining to it will be printed to the Terminal console when the server is started.

history Api Fallback property


				historyApiFallback: true,
						

When using the HTML5 History API (frequently used in React apps), index.html will probably have to be served instead of a 404 response. You enable this feature by passing true.

stats object


				stats: {
				  chunks: true,
				  modules: true
				},
						

Here, stats is an object unto itself with properties. It is part of my webpack-prod.config.js.

chunks property


				chunks: true,
						

Means that built modules information should be added to chunk information in Terminal.

modules property


				modules: true
						

This means that modules should be sorted by a field. In other words, information printed to console will be more visually organized and readable.

npm_lifecycle_event

I used npm_lifecycle_event in my webpack configs because it provided me with more options for how I write my npm scripts.

ENV message

I set the npm_lifecycle_event environment variable as the value of TARGET_ENV. That's so that I could print a message to Terminal stating whether I was running a development or production build.

Ternary Expression


				const TARGET_ENV = process.env.npm_lifecycle_event === 'build' ? 'production' : 'development';
						

I used a ternary expression because in my package.json scripts, I was only able to add TARGET_ENV=development in my serve script, and not TARGET_ENV=production in my build script.

'production'


				new webpack.DefinePlugin({
				  DEV: false,
				  'process.env.NODE_ENV': JSON.stringify('production')
				}),
						

The ternary expression took care of that, because 'production' appears in the new webpack.DefinePlugin call in my webpack-prod.config.js.

Dev vs Prod Envs

When I first started using webpack exclusively for React web apps, I created only one webpack.config.js file. But as I started needing more loaders, plugins, and environment customizations, I realized that separating my webpack-dev.config.js from my webpack-prod.config.js was the way to go.

Dev vs Prod Envs

Many developers take it even a step further and create a webpack-common.config.js file for code common to both development and production.

CRA approach

And CRA takes the scripts approach. A lot like creating scripts for Gulp or Grunt tasks.

Custom NPM Scripts


				"scripts": {
				  "test": "jest",
				  "clean": "rimraf dist",
				  "serve": "webpack-dev-server --config webpack-dev.config.js && TARGET_ENV=development",
				  "build": "npm run clean && webpack --config webpack-prod.config.js",
				  "deploy": "git subtree push --prefix dist origin gh-pages"
				},
						

You can create custom, aka local, npm scripts in your package.json file.

clean script


				script: {
				  "clean": "rimraf dist"
				}
						

Is executed in Terminal by running npm run clean. rimraf is an npm plugin which refers to the rm -rf comand. It cleans everything within the specified directory and all its subdirectories. It's much faster than the clean-webpack-plugin, which does the same thing.

serve script


				"script": {
				  "serve": "webpack-dev-server --config webpack-dev.config.js && TARGET_ENV=development"
				}
						

webpack-dev-server --config webpack-dev.config.js means the webpack-dev-server will run using the webpack dev config. TARGET_ENV=development ensures that my "Serving locally..." message gets printed to Terminal when I run this command.

build script


				"script: {
				  "build": "npm run clean && webpack --config webpack-prod.config.js",
				}"
						

First the old dist is removed with clean script, followed by a new build. "Serving for production ..." is still printed out to Terminal because of 'process.env.NODE_ENV': JSON.stringify('production') defined in the call to the Define Plugin.

uglifyjs webpack plugin

If I tried to use the uglifyjs-webpack-plugin in production using the build script, and I wanted to emit source-maps, I found that none were emitted. When I removed the webpack production config from the build script, then they were emitted. I came across other problems with the uglifyjs-webpack-plugin, but there is no time to discuss them now. I did, however, learn from the webpack docs that there are alternatives. I look forward to testing them out.

POSTCSS

Now to my favorite part. Configuring POSTCSS with webpack and React.

POSTCSS config


				module.exports = {
				  plugins: [
					require('postcss-import'),
					require('postcss-mixins'),
					require('postcss-simple-vars'),
					require('postcss-nested'),
					require('postcss-hexrgba'),
					require('autoprefixer')
				  ]
				}
						

In order to make POSTCSS work with webpack (and your React app), you have to create a postcss.config.js file.

POSTCSS in webpack.config


				module: {
				  rules: [
					{
					  test: /\.css$/,
					  use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
						fallback: 'style-loader',
						use: ['css-loader', 'postcss-loader']
					  })),
					}
				  ]
				}
						

css-hot-loader is for HMR of our extracted css, and we bring together our POSTCSS modules using .concat() into a new bundled css file.

Benefits

  • Allows for modularization (_partialization) of your css in development.
  • Instead of spreading out your css modules all over the place, i.e., next to the components they are styling, you place them in the same src folder in development.
  • You still end up with a single bundled css file in production.
  • Provides all sorts of cool features which make styling your app faster and easier.

Jest and React without CRA

  • Jest does not play nice with React JSX out of the box.
  • And It doesn't play nice with Babel out of the box either.
  • Both take some configuration.

Configuring Babel for Jest


				npm install babel-jest regenerator-runtime --save-dev
						

In configuring Babel for Jest, first install the babel-jest and regenerator-runtime npm packages.

.babelrc


				{
				  "presets": ["env", "stage-1", "stage-2", "jest", "react"]
				}
						

Then add all the babel presets you're using in your app in a .babelrc file which should reside in the root of your project along with your package.json.

babel-jest and babel-react

Are used to transform our code inside of the test environment.

NPM setup without CRA


				npm install jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer --save-dev
						

After you have installed all these devDependencies, you are almost ready to go. You are, however, ready to use Jest's Snapshot Testing.

Jest, React, and Static Assets


				"jest": {
				  "moduleNameMapper": {
					"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
					"\\.(css|less)$": "/__mocks__/styleMock.js"
				  }
				},
						

Placing the above code into your package.json lets Jest play nice with asset files such as stylesheets and images.

Mock files


				// __mocks__/styleMock.js

				module.exports = {};
						

				// __mocks__/fileMock.js

				module.exports = 'test-file-stub';
						

Usually, such files aren't really useful in tests, so we can safely mock them out.

identity-obj-proxy


				npm install --save-dev identity-obj-proxy
						

If you are using CSS modules, then it's better to mock a proxy for your className lookups.

First you install the identity-obj-proxy npm package. This package allows you to use an ES6 Proxy to mock CSS Modules.

package.json


				// package.json (for CSS Modules)
				"jest": {
				  "moduleNameMapper": {
				    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
				    "\\.(css|less)$": "identity-obj-proxy"
				  }
				}
						

"/__mocks__/styleMock.js" is replaced with "identity-obj-proxy". Great for Jest Snapshot Testing.

Wrap Up

When I first started working on my React workflow, it was some time back. I even left it for a bit and then returned to continue. Much has changed since then.

CRA

CRA has piled on more features and documentation! Whether you end up using it or not, it is worth following them on Github, visiting their issues threads, and becoming acquainted with their documentation. They even suggest alternatives!

Custom React App - Amido

React

The React team has been sensitive to developers' needs. For the longest time, they were discussing how to better serve developers in development and production. According to threads I came across on Github, many developers were deploying development instead of production code without realizing it. So React responded by adding a little icon to React DevTools that would signal to developers whether their application was deployed with development or production code.

React DevTools

If a React application is deployed with development code, the icon appears red. If it is deployed with production code, it appears black. In addition, a new warning appears in the browser console telling you if your application is running with minified development code rather than production.

webpack

webpack has drastically improved their documentation, making it easier to master. If you haven't already checked it out, you should. They're doing a really great job!

Bye Bye patents!

I learned last week via @wesbos that Facebook is relicensing React, Jest, Flow, and Immutable.js under the MIT license!

We're relicensing these projects because React is the foundation of a broad ecosystem of open source software for the web, and we don't want to hold back forward progress for nontechnical reasons. - Adam Wolff, Facebook Engineering

Resources

Resources contd

Resources contd