First Edition
Created by Maria D. Campbell / @letsbsocial1
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.
I'm Maria.
I've always been obsessed with development and construction in one way or another.
So why am I here today to discuss React workflows?
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.
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 ...
Sometimes it's faster (and easier) to start from scratch than try to manipulate to your needs.
And that's where
(2 and beyond) came in.
So why webpack?
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.
And if you are familiar with CRA, you know how extensive its boilerplate is.
if your focus is on writing JSX and JS for the browser.
with that approach is that when developers start heavily relying on tools like that, they can lose site of the bigger picture.
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.
I would get errors in my React projects which I sometimes found difficult to debug. They might or might not have been CRA related.
So I decided to start creating my own React workflows using webpack.
module.exports = {};
webpack is NOT a task runner. It is a module bundler.
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.
To get started, you only need to understand 4 basic concepts:
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.
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.
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.
And the main App Component that holds everything else related to
the app is rendered there as well.
module.exports = {
entry: {
bundle: './src/index.js',
vendor: VENDOR_LIBS
},
}
You may have noticed I add a second entry point called vendor.
const VENDOR_LIBS = [
'react', 'react-dom', 'prop-types'
];
Relates to non-webpack, 3rd party plugins.
is important for 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.
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: {
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.
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.
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).
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.
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!
I came to realize that simply adding [chunkhash] to my filename didn't exactly achieve what I thought it would out of the box.
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.
There are 3 main types of code in a typical app or site built with webpack:
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 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.
So why mention all of this? Because runtime and manifest affect browser caching.
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.
We can use the webpack CommonsChunkPlugin to extract webpack's boilerplate runtime and manifest.
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 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.
[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.
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.
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.
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.
So if we were to run a new build, only the [chunkhash] for bundle.js and runtime.js would change.
webpack loaders transform your files into modules as they are added to your dependency graph. Loaders have 2 main purposes:
test property)
bundle.js. (use property)
rules: [
{
test: /\.js?/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'stage-1', 'stage-2', 'jest', 'react']
}
},
exclude: /node_modules/
},
]
test: /\.js?/,
The test property tells webpack what kind of file to match to the babel-loader and its preset options,
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: /node_modules/
The exclude property means that whatever modules match the /node_modules/ regex should be excluded from bundle.js.
test: /\.css$/,
{
test: /\.css$/,
use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'postcss-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.
HMR exchanges, adds, or removes modules while an app is
running, without a full reload. It can speed up development because:
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] 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.
import logo from './logo.svg';
Welcome to React with Redux
Image loader module for webpack, and this is what it permits you to do in your React Component.
{
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.
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.
const htmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
]
In order to use a plugin, you have to require it,
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.
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.
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.
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.
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.
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.
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.
new webpack.DefinePlugin({
DEV: true
}),
Allows you to create global constants which can be configured at compile time.
new webpack.DefinePlugin({
DEV: false,
'process.env.NODE_ENV': JSON.stringify('production')
}),
Allows for different behavior between development and production builds.
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: '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: {
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: 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: '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: true,
Means that the webpack-dev-server will open your default browser when it launches.
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.
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.
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: {
chunks: true,
modules: true
},
Here, stats is an object unto itself with properties. It is part of my webpack-prod.config.js.
chunks: true,
Means that built modules information should be added to chunk information in Terminal.
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.
I used npm_lifecycle_event in my webpack configs because it provided me with more options for how I write my npm scripts.
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.
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.
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.
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.
Many developers take it even a step further and create a webpack-common.config.js file for code common to both development and production.
And CRA takes the scripts approach. A lot like creating scripts for Gulp or Grunt tasks.
"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.
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.
"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.
"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.
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.
Now to my favorite part. Configuring POSTCSS with webpack and React.
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.
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.
npm install babel-jest regenerator-runtime --save-dev
In configuring Babel for Jest, first install the babel-jest and regenerator-runtime npm packages.
{
"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.
Are used to transform our code inside of the test environment.
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": {
"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.
// __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.
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 (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"
}
}
" is replaced with "identity-obj-proxy". Great for Jest Snapshot Testing.
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 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!
Customising @reactjs create-react-app has been super useful; I've written a blog post for @WeAreAmido about it https://t.co/JNEPSQJmIT
— Callum Mellor-Reed (@callummr) September 19, 2017
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.
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 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!
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