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