Build a React Application from First Principles - Improving Workflow

This is Part 11 of the series - Build a React Application from First Principles.

Get the completed code from the last step on GitHub and follow along!

So far, whenever we make a change to the code, we'd have to run webpack again, and, depending on which server you are running, restart the web server.

In the long run, it'll save us a lot of time if we can automate this process.

Luckily for us, Webpack can watch for changes within files, and npm has scripts we can define.

Webpack Watch

Webpack can watch a file and recompile the code whenever it detects changes are made. All we need to do is add a watch: true value to webpack.config.babel.js.


export default {
  entry: {...},
  output: {...},
  module: {...},
  watch: true,
};
Now, when we run Webpack again, it will compile the code once, and then watch for changes.
$ ./node_modules/.bin/webpack

Webpack is watching the files…

Hash: 732f931849f2165dce55
Version: webpack 2.2.0
Time: 2092ms
           Asset     Size  Chunks                    Chunk Names
./dist/bundle.js  1.05 MB       0  [emitted]  [big]  app
   [0] ./~/process/browser.js 5.3 kB {0} [built]
   ...
    + 175 hidden modules

Now go ahead and make a small change (E.g. change the word 'posts' in 'This is where the posts will go!' to 'articles'). When you save the change, Webpack detects this and recompile the code again.

Hash: 623dddcc2cb33508208e
Version: webpack 2.2.0
Time: 264ms
           Asset     Size  Chunks                    Chunk Names
./dist/bundle.js  1.05 MB       0  [emitted]  [big]  app
    + 190 hidden modules

Creating Multiple Webpack Configuration

Whilst we are developing, we'd want the watch functionality. But when we are building for production, we want Webpack to just build. Later on, we might also want to define some optimization steps that would take too long for development, such as minification / uglification, compression of images etc.

At the moment, we only have one babel.config.babel.js file. In order to have different types of builds, we need to have multiple configuration files, and passing which configuration we want to use using the --config flag.

So let's rename our webpack.config.babel.js to webpack.dev.babel.js. Next, duplicate the file and name it webpack.prod.babel.js. Remove the watch property from the production build.

Now, when we are developing, we can run:

$ ./node_modules/.bin/webpack --config webpack.dev.babel.js

When we want to build for production, we can use

$ ./node_modules/.bin/webpack --config webpack.prod.babel.js

npm scripts

Typing ./node_modules/.bin/webpack --config webpack.dev.babel.js and python -m SimpleHTTPServer 1234 on two separate terminals each time you want to develop is bothersome.

Also, having to remember how to set up the project might not be obvious neither. This is a relatively small project, but larger projects might have many dependencies and environment variables that needs to be set before the app can work.

We can, instead, use npm scripts to simplify all these commands, so the next time we clone the project and want to set it up, we can simple run yarn install and it will run the script to install all dependencies. Likewise, we can run yarn start to watch the files and start the web server.

Background

You run npm scripts using yarn run <name>.

As you'll see later, you can name your scripts however you like, but there are several script names that have special meaning:

  • publish - prepublish, publish, postpublish
  • install - preinstall, install, postinstall
  • uninstall - preuninstall, uninstall, postuninstall
  • version bump - preversion, version, postversion
  • test - pretest, test, posttest
  • stopping the application prestop, stop, poststop
  • starting the application - prestart, start, poststart
  • restarting the application - prerestart, restart, postrestart

For example, when you run yarn run start, the command specified in prestart and poststart will run either side of the start command.

We should use these pre-defined script names when appropriate, but we can also define our own.

Adding start script

Open up package.json and make the following changes:

{
  "devDependencies": {...},
  "dependencies": {...},
  "scripts": {
    "start-server": "python -m SimpleHTTPServer 5465",
    "start-watch": "./node_modules/.bin/webpack --config webpack.dev.babel.js"
  }
}

Now we can run yarn run start-watch in one terminal, and yarn run start-server on another terminal and we can start developing. The commands are much shorter now!

However, we can go one step further and run them both with just one command. But since these two scripts do not terminate, we need to run them concurrently. Luckily, there's the concurrently package which enables us to do just that.

$ yarn add concurrently --dev

Next, add a new entry to scripts

{
  "devDependencies": {...},
  "dependencies": {...},
  "scripts": {
    "start": "yarn install && concurrently --kill-others \"npm run start-watch\" \"npm run start-server\"",
    "start-server": "python -m SimpleHTTPServer 5465",
    "start-watch": "./node_modules/.bin/webpack --config webpack.dev.babel.js"
  }
}

We run yarn install to ensure all dependencies are installed. If all dependencies are already installed, this step takes around a second, so it would hinder our workflow too much.

Next, concurrently will emulate the effect of running both commands concurrently, as if it was one two terminals.

Run yarn run start (or use its special shorthand yarn start) and start developing!

$ yarn start
yarn start v0.19.1
$ concurrently --kill-others "npm run start-watch" "npm run start-server"
[0]
[0] > @ start-watch /home/administrator/sandbox/grapevine
[0] > webpack --config webpack.dev.babel.js
[0]
[1]
[1] > @ start-server /home/administrator/sandbox/grapevine
[1] > python -m SimpleHTTPServer 5465
[1]
[0]
[0] Webpack is watching the files…
[0]
[0] Hash: 732f931849f2165dce55
[0] Version: webpack 2.2.0
[0] Time: 1725ms
[0]            Asset     Size  Chunks                    Chunk Names
[0] ./dist/bundle.js  1.05 MB       0  [emitted]  [big]  app
[0]    [0] ./~/process/browser.js 5.3 kB {0} [built]
[0]    [2] ./~/fbjs/lib/warning.js 2.1 kB {0} [built]
[0]   [15] ./~/react/lib/ReactElement.js 11.2 kB {0} [built]
[0]   [17] ./~/react-dom/index.js 59 bytes {0} [built]
[0]   [18] ./~/react/react.js 56 bytes {0} [built]
[0]   [21] ./~/react/lib/React.js 2.69 kB {0} [built]
[0]   [22] ./~/firebase/firebase-browser.js 259 bytes {0} [built]
[0]   [24] ./~/firebase/app.js 16.4 kB {0} [built]
[0]  [103] ./~/firebase/auth.js 119 kB {0} [built]
[0]  [104] ./~/firebase/database.js 121 kB {0} [built]
[0]  [105] ./~/firebase/messaging.js 16.9 kB {0} [built]
[0]  [106] ./~/firebase/storage.js 29 kB {0} [built]
[0]  [120] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
[0]  [177] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
[0]  [189] ./src/index.jsx 885 bytes {0} [built]
[0]     + 175 hidden modules

So if I make a change to one of the files, Webpack watch would pick that up and the results printed in the terminal.

[0]  [189] ./src/index.jsx 885 bytes {0} [built]
[0]     + 175 hidden modules
[0] Hash: 623dddcc2cb33508208e
[0] Version: webpack 2.2.0
[0] Time: 232ms
[0]            Asset     Size  Chunks                    Chunk Names
[0] ./dist/bundle.js  1.05 MB       0  [emitted]  [big]  app
[0]     + 190 hidden modules

Likewise, if I make a new request for the page, it would print that out on the terminal too.

[0] ./dist/bundle.js  1.05 MB       0  [emitted]  [big]  app
[0]     + 190 hidden modules
[1] 127.0.0.1 - - [25/Jan/2017 20:04:11] "GET / HTTP/1.1" 200 -
[1] 127.0.0.1 - - [25/Jan/2017 20:04:11] "GET /dist/bundle.js HTTP/1.1" 200 -

The --kill-others flag tells concurrently to kill the other processes if any one of them exits or die. For instance, when we press Ctrl + C, we want both the watch and the server to stop running.

Now, whenever we want to start development, all we need to do is run yarn run start!

Production Build

Lastly, we should add a script that we'd use to build the application for production. It will simply run Webpack with the production configuration.

"scripts": {
  "build": "./node_modules/.bin/webpack --config webpack.prod.babel.js",
  "start": "yarn install && concurrently --kill-others \"npm run start-server\" \"npm run start-watch\" ",
  "start-server": "python -m SimpleHTTPServer 5465",
  "start-watch": "./node_modules/.bin/webpack --config webpack.dev.babel.js"
}

Now, we can build our app using a simpler, shorter command.

$ yarn run build
yarn run v0.19.1  
$ ./node_modules/.bin/webpack --config webpack.prod.babel.js
Hash: 732f931849f2165dce55  
Version: webpack 2.2.0  
Time: 1646ms  
           Asset     Size  Chunks                    Chunk Names
./dist/bundle.js  1.05 MB       0  [emitted]  [big]  app
   [0] ./~/process/browser.js 5.3 kB {0} [built]
   [2] ./~/fbjs/lib/warning.js 2.1 kB {0} [built]
  [15] ./~/react/lib/ReactElement.js 11.2 kB {0} [built]
  [17] ./~/react-dom/index.js 59 bytes {0} [built]
  [18] ./~/react/react.js 56 bytes {0} [built]
  [21] ./~/react/lib/React.js 2.69 kB {0} [built]
  [22] ./~/firebase/firebase-browser.js 259 bytes {0} [built]
  [24] ./~/firebase/app.js 16.4 kB {0} [built]
 [103] ./~/firebase/auth.js 119 kB {0} [built]
 [104] ./~/firebase/database.js 121 kB {0} [built]
 [105] ./~/firebase/messaging.js 16.9 kB {0} [built]
 [106] ./~/firebase/storage.js 29 kB {0} [built]
 [120] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
 [177] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
 [189] ./src/index.jsx 885 bytes {0} [built]
    + 175 hidden modules
Done in 2.50s.  

For production, we'd usually want to minify our code, so let's use a Webpack plugin to do that.

In case you forgot... Webpack have loaders and plugins. Loaders process the source files individually, and plugins process the bundled file. We're using a plugin because minification works best when applied to a large chunk of code.

Plugins are usually just standard npm packages. You can require or import them and instantiate the plugin using new <plugin>(). The most common ones, like minification, are already packaged with Webpack.

import webpack from 'webpack';

export default {
  entry: {...},
  output: {...},
  module: {...},
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
  ]
};
$ yarn run build
yarn run v0.19.1  
$ ./node_modules/.bin/webpack --config webpack.prod.babel.js
Hash: 732f931849f2165dce55  
Version: webpack 2.2.0  
Time: 9120ms  
           Asset    Size  Chunks                    Chunk Names
./dist/bundle.js  528 kB       0  [emitted]  [big]  app
   [0] ./~/process/browser.js 5.3 kB {0} [built]
   [2] ./~/fbjs/lib/warning.js 2.1 kB {0} [built]
  [15] ./~/react/lib/ReactElement.js 11.2 kB {0} [built]
  [17] ./~/react-dom/index.js 59 bytes {0} [built]
  [18] ./~/react/react.js 56 bytes {0} [built]
  [21] ./~/react/lib/React.js 2.69 kB {0} [built]
  [22] ./~/firebase/firebase-browser.js 259 bytes {0} [built]
  [24] ./~/firebase/app.js 16.4 kB {0} [built]
 [103] ./~/firebase/auth.js 119 kB {0} [built]
 [104] ./~/firebase/database.js 121 kB {0} [built]
 [105] ./~/firebase/messaging.js 16.9 kB {0} [built]
 [106] ./~/firebase/storage.js 29 kB {0} [built]
 [120] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
 [177] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
 [189] ./src/index.jsx 885 bytes {0} [built]
    + 175 hidden modules
Done in 9.93s.  

Run yarn run start-server and look at the bundle.js, it has been minified!

However, in the console, you might see a warning

Warning: It looks like you're using a minified copy of the development build of React. When deploying React apps to production, make sure to use the production build which skips development warnings and is faster. See https://fb.me/react-minification for more details.

To fix this, all we need to do is add a React-specific plugin to our Webpack config.

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify('production')
    }
  }),
  new webpack.optimize.UglifyJsPlugin(),
]
$ yarn run build
yarn run v0.19.1  
$ ./node_modules/.bin/webpack --config webpack.prod.babel.js
Hash: 013329ee869492477aa1  
Version: webpack 2.2.0  
Time: 9090ms  
           Asset    Size  Chunks                    Chunk Names
./dist/bundle.js  451 kB       0  [emitted]  [big]  app
   [3] ./~/object-assign/index.js 2.11 kB {0} [built]
  [12] ./~/react-dom/index.js 59 bytes {0} [built]
  [13] ./~/react/react.js 56 bytes {0} [built]
  [17] ./~/react/lib/React.js 2.69 kB {0} [built]
  [20] ./~/firebase/firebase-browser.js 259 bytes {0} [built]
  [22] ./~/firebase/app.js 16.4 kB {0} [built]
  [98] ./~/firebase/auth.js 119 kB {0} [built]
  [99] ./~/firebase/database.js 121 kB {0} [built]
 [100] ./~/firebase/messaging.js 16.9 kB {0} [built]
 [101] ./~/firebase/storage.js 29 kB {0} [built]
 [115] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
 [169] ./~/react/lib/ReactClass.js 26.5 kB {0} [built]
 [170] ./~/react/lib/ReactDOMFactories.js 5.53 kB {0} [built]
 [171] ./~/react/lib/ReactPropTypes.js 15.8 kB {0} [built]
 [177] ./src/index.jsx 885 bytes {0} [built]
    + 163 hidden modules
Done in 10.00s.  

Now, when you run yarn run start-server, the warning would be gone.

Summary

In this article, we've streamlined our development process by specifying a watch function that watches for file changes and recompile our code during development. We then improved on this and use npm scripts to combine multiple commands into one. Lastly, we split our scripts into two version - development and production - and added minification steps to the production build.

Finally, get the complete source code for this article on GitHub!

Daniel Li

Full-stack Web Developer in Hong Kong. Founder of Brew.

Hong Kong http://danyll.com