React - starting from scratch with Webpack, Babel and ESLint
Contents
What is this about
About a month ago, I decided to learn React and begun reading Getting started articles on the subject. It soon became apparent that what I was embarking on was not just learning React, but also other tools like Webpack, Babel or Gulp.
Boilerplates and Generators
It was overwhelming to learn all those tools in parallel, so I settled for one of these boilerplate React projects. I recently learned that there is a generator from Facebook called create-react-app. If you want to read more about this kind of generators - Andrew Farmer's post is one of the best resources I found.
Starting from scratch
Starting from scratch may seem weird if you're coming from Rails or Ember, where the frameworks already come with app generators.
Personally, I see React & ecosystem as Sinatra - one can either utilize boilerplates/generators, or start from scratch and piece together all the required libraries.
Hopefully, in the future React will get an official react-cli (similar with ember-cli). But this is a completely different discussion. In the meantime, I think starting from scratch is a better approach if you're just getting started.
My starting point was Jonathan Verrecchia's JavaScript Stack from Scratch Github tutorial. I learned a lot by following along, but I was also impatient. My focus was on getting the set-up out of the way as quickly as possible, so I could start coding my learning React app. I skimmed through the guide, without taking the time to learn Webpack and Gulp. Jonathan's guide is just a foundation, which one would needto build on top of for things like:
- Live code reloading
- Stylesheets
- Production builds with minification
- etc.
In hindsight, this is the plan I wish I followed for mastering React:
- use only
webpack
to start, instead ofwebpack+gulp
(one less thing to learn) - use
webpack-dev-server
andhot-module-replacement
for live updates in the browser - add
ESLint
loader that runs on every update - process both, vendor css and my custom css
- create a production build.
Webpack
Let's start with the basics. Webpack is a module bundler that can be installed globally or as part of a project, using NPM.
npm init
npm install --save-dev webpack
Let's create the bare minimum webpack.config.js
file that will bundle an source.js file into an output:
module.exports = {
entry: "./entry.js",
output: {
path: __dirname,
filename: "bundle.js"
}
};
The entry
can be a string or an array. It is the starting point for Webpack to follow imports and bundle everything together into the output
specified. For simplicity's sake at this step, there is only a path
pointing to the current folder and a filename
.
This is our entry.js file:
console.log('Hello World!');
Next, let's create an index.html
file and load the bundle.js
file.
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type="text/javascript" src="bundle.js" charset="utf-8"></script>
</body>
</html>
Running Webpack generates the bundle.js
as expected. Notice that Webpack generates a hash for every build - this will come in handy when dealing with asset caching.
$ webpack
Hash: 26a59627d47eefca08e4
Version: webpack 1.14.0
Time: 42ms
Asset Size Chunks Chunk Names
bundle.js 1.42 kB 0 [emitted] main
[0] ./entry.js 29 bytes {0} [built]
External Modules
Here, let's pretend to write a module and import it into the entry.js
file to verify how Webpack bundles that.
Create a new file and call it message.js
module.exports = {
sayHello: function() {
return "Hello World!";
}
};
Update the entry.js file:
var message = require('./message');
document.write(message.sayHello());
Now if we open index.html
we should see 'Hello World!'.
Webpack-dev-server
It is inconvenient to repeatedly run webpack
and refresh the index.html file. Instead, let's set up Webpack to watch for changes in the files and bundle those with every change. Webpack-dev-server runs a simple express app to serve content and refresh listens for changes on the files using WebSockets.
Install webpack-dev-server:
npm install --save-dev webpack-dev-server
The node server will run on port 8080 by default. This can be changed in the webpack.config.js
:
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
devServer: {
port: 8080
},
};
Now I can run webpack-dev-server
and browse to http://localhost:8181 to see the app.
Let's add the --hot option to enable Hot Module Replacements (this means that adding, removing or updating modules will not require restarts and will automatically reload the page). The --inline option includes the webpack-dev-server in the generated bundle.js file:
webpack-dev-server --hot --inline
Bam! The live updates are now available!
Finally, add this to the package.js scripts
:
{
"name": "webpack_blogpost",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --hot --inline"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^1.14.0",
"webpack-dev-server": "^1.16.2"
}
}
Styles
So far there was no need to use loaders
to handle the JS content, because we only had a simple entry file with a loaded module. Next, let's look at two loaders to bundle up the stylesheets: style-loader and css-loader. The first loader injects a <style>
element in the served content (from webpack-dev-server) and the latter resolves imports, similar to how webpack resolves js module imports.
npm install --save-dev css-loader style-loader
Add two types of stylesheets typically used in a React app: a custom one for the app and a framework, such as Bootstrap.
Let's create the app.css
:
body {
background: #efefef;
}
Now update the webpack.config.js
and add the css and style loaders:
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: "style!css"
}
]
},
devServer: {
port: 8181
},
};
~
Install Bootstrap (as of this writing, the latest alpha release for v4 is alpha.6).
npm install --save bootstrap@4.0.0-alpha.6
Now I can require both, bootstrap and the stylesheet, in the entry.js
file:
var message = require('./message');
require('bootstrap/dist/css/bootstrap.css');
require('./app.css');
document.write(message.sayHello());
Running npm start
and checking the result, I see two <style>
tags - one for each stylesheet imported.
Note that now the bundle.js
almost doubled in size (from about 270Kb to about 470kB). Keeping in mind that none of the CSS code was minified, this is still a massive jump to be be aware of. In practice, this will probably not occur and the vendor css would be bundled separately - more about this in the Production Build section
ES6
With the working Webpack app, it is a good time to use some of the nicer ES6 features and update the code. Use Babel to compile the ES6 enabled code to JS code, which most browsers can run.
npm install --save-dev babel-loader babel-core babel-preset-latest
As different versions of Javascript get approved by ECMA International, they are given nicknames, such as ES2015 and ES2016, or the latest ES2017. Babel's latest-preset page offers more up to date details. These presets are specified using a query
object as part of the loader.
Similar to how the style and css loaders were used, let's use the babel-loader
to process any of the matched files in the test
regular expression.
Here is the updated webpack.config.js
:
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: "style!css"
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
'presets': ['latest']
}
}
]
},
devServer: {
port: 8181
},
};
Now let's change all the requires
to imports
:
var message = require("./message");
require("./app.css");
to produce:
import message from './message';
import './app.css';
React
It is finally time to add React to the app. Let's install all the required modules:
npm install --save react react-dom
Next, install the React loader and the Babel preset for compiling React:
npm install --save-dev react-hot-loader babel-preset-react
Now, update the webpack.config.js
and add the loaders and Babel presets:
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style!css'
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
}
]
},
devServer: {
port: 8181
},
};
Note that I removed the query
from the jsx loader, because this can only be used with one loader. After adding the react-hot
, let's define the presets inline. If the presets become more complex, refer to something like webpack-combine-loaders.
Let's create a sample component called App.jsx
and import it. Also, use render code and update the HTML element, where the React app will mount.
// App.jsx
import React from 'react';
export default class App extends React.Component {
render() {
return (
<button className="btn btn-primary">Press me!</button>
);
}
}
// entry.js
import { render } from 'react-dom';
import React from 'react';
import App from './App.jsx';
import 'bootstrap/dist/css/bootstrap.css';
import './app.css';
render(
<App />,
document.getElementById('app')
);
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="bundle.js" charset="utf-8"></script>
</body>
</html>
Everything should bundle without errors and the Bootstrap button should be rendered in the browser.
ESLint
npm install --save-dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-loader eslint-plugin-react
Next, add a loader to Webpack and create the ESLint config file (but keep it separate from the Webpack config - .eslintrc)
// webpack.config.js
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style!css'
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['eslint-loader']
}
]
},
resolve: {
extensions: ['', '.js', '.jsx']
},
devServer: {
port: 8181
},
};
// .eslintrc
{
"extends": "airbnb",
"env": {
"browser": true,
},
"plugins": [
"react"
]
}
At this point, running npm start
generates a handful of errors in entry.js
and App.jsx
:
entry.js
4:17 error Unexpected use of file extension "jsx" for "./App.jsx" import/extensions
5:8 error Absolute imports should come before relative imports import/first
9:3 error JSX not allowed in files with extension '.js' react/jsx-filename-extension
10:33 error Missing trailing comma comma-dangle
App.jsx
3:16 error Component should be written as a pure function react/prefer-stateless-function
Go ahead and fix these errors. Refer to my repo for the current state of the app. I updated my Webpack script to build the app into its own folder and added it to my .gitignore.
build
src
|_ app.css
|_ App.jsx
|_ entry.jsx
|_ index.html
|_ message.js
package.json
webpack.config.js
.gitignore
.eslintrc
.babelrc
This is my new folder structure. Note that the index.html needs to be copied over to the build folder. I have two options for that:
- use a file loader and require it in my entry.jsx
- use the html-webpack-plugin
Using first option:
npm install --save-dev file-loader
Remember how the style loaders worked? The file loader looked for import statements matching a file, but instead of bundling the contents into the index file it copied that file to the build location:
import { render } from 'react-dom';
import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';
import './styles/app.css';
import './index.html'
render(
<App />,
document.getElementById('app'),
);
// webpack.config.js
...snip
{
test: /\.html$/,
loader: 'file?name=[name].[ext]'
}
...snip
Going forward, the loader will copy any html file imported into the app to the output folder. There is no change when running the webpack-dev-server, because everything is stored in memory. However, running webpack
in the app folder will generate the build folder along with the index.html
file.
Source Maps
One last thing to add before moving on to the Production build is the source maps for the bundled JS files.
When bundling everything and running JS in the browser, I cannot detect the exact line in the source code in case of an error. Source maps solve this problem by creating separate files for either JS or CSS.
// webpack.config.js
module.exports = {
...snip
devtool: 'source-map',
...snip
}
Production Build
Let's create a separate Webpack config for production and call it webpack.production.config.js
:
module.exports = {
entry: './src/entry.jsx',
devtool: 'cheap-module-source-map',
output: {
path: __dirname + '/build',
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: 'style!css'
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['eslint-loader']
},
{
test: /\.html$/,
loaders: ['file?name=[name].[ext]']
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin({
minimize: true,
warnings: false,
})
],
resolve: {
extensions: ['', '.js', '.jsx']
}
};
Here, minify the bundled JS, set the environment to production and use a different source map. The cheap-module-source-map
is better for production bundling, according to the official docs:
cheap-module-source-map - A SourceMap without column-mappings. SourceMaps from loaders are simplified to a single mapping per line.
Recalling how the stylesheets were used previously, I do not recommend bundling them together with the JS in the production build. Instead, let's extract all CSS files and output a separate minified bundle.css
. Use a Webpack plugin called ExtractTextPlugin.
This is the final webpack config. Just add the link tobundle.css
to the src/index.html
file and...done!
// webpack.production.config.js
var webpack = require('webpack');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: './src/entry.jsx',
devtool: 'cheap-module-source-map',
output: {
path: __dirname + '/build',
publicPath: __dirname + '/build/static',
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader")
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['eslint-loader']
},
{
test: /\.html$/,
loaders: ['file?name=[name].[ext]']
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin({
minimize: true,
warnings: false,
}),
new ExtractTextPlugin("bundle.css")
]
resolve: {
extensions: ['', '.js', '.jsx']
}
};
Conclusion
Looking back, I can now see why some prefer to use Gulp and Webpack together. The last step, where the css was bundled together, would have been a better fit for a Gulp task. My next post will focus on adding Gulp to the mix and improving the setup.
Overall, I believe it is good practice to start simple and progressively add tools as needed. Gulp is a great example, along with== more improvements / tools I could have used.
Hope you enjoyed this as much as I did! Here's the Github repo with the final application: