Minimum Viable Webpack, Babel, React Setup WITHOUT “create-react-app”

--

It seems that most developers using React recently are only using “create-react-app” when starting a new project. This is completely fine for most projects, but at some point you are probably going to need to make some changes to Webpack as your project grows.

This means that you will eventually need to “eject” the Webpack configuration from the project. If you haven’t done anything with Webpack before this can turn into a headache trying to get what you need done.

In this article I will show a basic setup that will bundle your files, create different build processes for development/production, include hot loading, use babel, and uglify your build code. This is what is, in my opinion a minimum viable setup.

We are going to build this from scratch. NO BOILERPLATE! I will be doing this using Ubuntu. Mac should also be fine. I will be using Yarn rather than npm in this tutorial. Most commands are very similar… use npm if you wish and are comfortable with it.

Lets start with creating our project directory:

mkdir min-viable
cd min-viable

Next lets initialize our package.json:

yarn init -y

We are going to need the following packages for our setup:

Dependencies:

  • react
  • react-dom

DevDependencies:

  • webpack
  • webpack-dev-server Serves up our dev build during development
  • webpack-merge Allows us to combine config files for Webpack (will make sense later I promise!)
  • babel-core We need Babel to ensure that classes etc work in React. We also need it to allow other features in React that may not be in older browsers.
  • babel-preset-es2015 Plugin for Babel ensuring we can use ES6 functions/syntax
  • babel-preset-react Allows us to use JSX and other react features alongside ES6
  • css-loader Helps Webpack find and import css files. Allows the import syntax for stylesheets… neat
  • babel-loader Loads Babel into Webpack build process which turns your ES6 code into backwards compatible code (ES5 by default)
  • react-hot-loader Enables hot loading during development (page reloads when you makes changes to code)
  • clean-webpack-plugin Deletes the build directory before creating new build ensuring there aren’t any weird artifacts from previous builds
  • html-webpack-plugin Creates an index.html file on which you will mount your React app.
  • uglifyjs-webpack-plugin Uglifies your code

Run the following command in your terminal:

yarn add babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2 clean-webpack-plugin css-loader html-webpack-plugin react-hot-loader style-loader uglifyjs-webpack-plugin webpack webpack-dev-server webpack-merge --dev

Those were only the development dependencies. Lets add our real dependencies:

yarn add react react-dom

Your package.json should now look something like this:

{
"name": "min-viable",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"clean-webpack-plugin": "^0.1.16",
"css-loader": "^0.28.7",
"html-webpack-plugin": "^2.30.1",
"react-hot-loader": "^1.3.1",
"style-loader": "^0.18.2",
"uglifyjs-webpack-plugin": "^0.4.6",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.8.2",
"webpack-merge": "^4.1.0"
},
"dependencies": {
"react": "^15.6.1",
"react-dom": "^15.6.1"
}
}

Next lets create our config files. The plan here is to have three config files:

  • webpack.config.common.js
  • webpack.config.dev.js
  • webpack.config.prod.js

Dev and Prod files will use common functionality from the Common file.

Lets create all three of them now in the terminal. These should be in the project’s root directory We will edit them soon:

touch webpack.config.common.js webpack.config.dev.js webpack.config.prod.js

Your directory structure should look as follows now:

/node_modules
package.json
webpack.config.common.js
webpack.config.dev.js
webpack.config.prod.js
yarn.lock

Create a Common Configuration File

Lets start with webpack.config.common.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
},
plugins: [
new CleanWebpackPlugin(['app']),
new HtmlWebpackPlugin({
title: 'Minimum-Viable',
filename: 'index.html',
template: './public/index.html',
}),
],
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'es2015',
'react',
],
},
},
exclude: /node_modules/,
},
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'es2015',
'react',
],
},
},
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'app'),
},
};

The file above of course warrants some explanation. I will explain each of the properties of this object below:

Entry

Webpack works by checking which files you are importing from where. It takes every file that has been linked to from another file and bundles them together into one file (at least by default… there are other ways to configure Webpack). Webpack needs to know where to start looking for these imported files. This is most commonly going to be index.js for React apps. This will be located in our “/src” folder just like in create-react-app. We will create the index.js file in a minute.

Plugins

Plugins do things not explicitly related to pre-processing of your files. In our minimum viable setup, we are going to delete the build directory and create a new index.html file in the new build directory. Note that we have listed a template in our HtmlWebpackPlugin. We will make that in a minute.

Module

This is where we include our loaders and the logic regarding when those loaders should run. Loaders perform pre-processing on our files. In our case we are running our code through babel to work with ES5 and loading in styles.

We have “exclude” to ignore node_modules, “test” with a regex to decide when to run the loader, and “use” to declare what loader to use. Inside use we are setting Babel presets to use.

Output

This is where the resulting build location should be declared. Here we are telling Webpack we want the resulting bundle of all of our JS files to be called “app.bundle.js” and that it should be put in the app directory (this will be created by Webpack).

The name “app.bundle.js” comes from the “[name]” part which is taken from the property name in entry (which is app).

Create a Development Configuration File

The last config file we created will be used for both production and development builds. However we are going to do a few more things depending on if we are in production or development mode.

Lets create the development config file:

const merge = require('webpack-merge');
const common = require('./webpack.config.common.js');
const webpack = require('webpack');
const path = require('path');
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
hot: true,
contentBase: path.resolve(__dirname, 'app'),
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
module: {
rules: [
{
test: /\.jsx$/,
use: 'react-hot-loader'
}
]
}
});

Explanation of the above file is as follows:

merge

We are taking our webpack.config.common.js file and merging it with our dev file. Keep it DRY!

devtool

This allows us to see errors that map to the code that we actually wrote. Remember that when the app is being served everything is minified and bundled. You need source maps to be able to see where the errors are as you wrote the code… not bundled. This allows for that.

devServer

This allows you to use the Webpack dev server which will enable hot loading. “hot” indicates that you will be using hot modules for hot loading and “contentBase” tells the server where to serve content from.

plugins

We are enabling hot loading with this plugin. We are doing the same with the loader.

Create a Production Configuration File

Lets do the same thing for our production file:

const merge = require('webpack-merge');
const common = require('./webpack.config.common.js');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = merge(common, {
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production'),
},
}),
new UglifyJSPlugin(),
],
});

We have a similar process as the development configuration file. Here we are instead uglifying our code and setting the mode to production. We are of course merging our production configuration file with our common one.

Create our Missing Files

We declared some paths in our config files that we still have not created. Lets fix that now (run from project root):

mkdir src
touch ./src/index.js
mkdir public
touch ./public/index.html

Your project directory should now look like this:

/node_modules
/public
index.html
/src
index.js
package.json
webpack.config.common.js
webpack.config.dev.js
webpack.config.prod.js
yarn.lock

Lets edit our index.html file:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>Min-Viable</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app"></div>
</body>
</html>

This will act as the template that will be put into our build directory each time we create a build.

Lets make our index.js file:

import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<h1>Hey Buddy</h1>,
document.getElementById('app'),
);
if (module.hot) {
module.hot.accept();
}

You should be familiar with this code if you are used to working with React. We are just rendering out our app to the index.html file by finding the “#app” div. We then need to add the bit of code at the bottom to enable hot loading.

We should now be ready to finally use our setup! Lets try running our server and see how it goes:

webpack-dev-server --config webpack.config.dev.js

You should see something similar to this as output:

clean-webpack-plugin: /home/tovarishfin/medium-tuts/webpack/app has been removed.
Project is running at http://localhost:8080/
webpack output is served from /
Content not from webpack is served from /home/tovarishfin/medium-tuts/webpack/app
Hash: 626a4c5b5fbbc4074aff
Version: webpack 3.6.0
Time: 1562ms
Asset Size Chunks Chunk Names
app.bundle.js 2.84 MB 0 [emitted] [big] app
index.html 444 bytes [emitted]
[50] (webpack)/hot/log.js 1.04 kB {0} [built]
[84] (webpack)/hot/emitter.js 77 bytes {0} [built]
[117] multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./src/index.js 52 bytes {0} [built]
[118] (webpack)-dev-server/client?http://localhost:8080 7.27 kB {0} [built]
[119] ./node_modules/url/url.js 23.3 kB {0} [built]
[125] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
[127] ./node_modules/loglevel/lib/loglevel.js 7.74 kB {0} [built]
[128] (webpack)-dev-server/client/socket.js 1.04 kB {0} [built]
[160] (webpack)-dev-server/client/overlay.js 3.71 kB {0} [built]
[165] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
[167] (webpack)/hot/dev-server.js 1.61 kB {0} [built]
[168] (webpack)/hot/log-apply-result.js 1.31 kB {0} [built]
[169] ./src/index.js 466 bytes {0} [built]
[170] ./node_modules/react/react.js 56 bytes {0} [built]
[186] ./node_modules/react-dom/index.js 59 bytes {0} [built]
+ 257 hidden modules
Child html-webpack-plugin for "index.html":
1 asset
[0] ./node_modules/html-webpack-plugin/lib/loader.js!./public/index.html 755 bytes {0} [built]
[1] ./node_modules/lodash/lodash.js 540 kB {0} [built]
[2] (webpack)/buildin/global.js 509 bytes {0} [built]
[3] (webpack)/buildin/module.js 517 bytes {0} [built]
webpack: Compiled successfully.

You should be able to see your new project running on “http://localhost:8080” Press ctrl + c to exit when you are ready to move on from this amazing and beautiful page.

Let’s now check that our build for production works:

webpack --config ./webpack.config.prod.js

You should now have an app folder in your project root directory with the following structure:

/app
app.bundle.js
index.html

Take a look at the index.html file:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>Min-Viable</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app"></div>
<script type="text/javascript" src="app.bundle.js"></script></body>
</html>

You can see that the bundled file from webpack has been added into the index.html file. Good to go!

Add Scripts to package.json

Let’s now make it easy to run our most common commands. We are going to need to update our package.json file:

{
"name": "min-viable",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "webpack-dev-server --config ./webpack.config.dev.js --progress --colors",
"build": "webpack --config ./webpack.config.prod.js --progress --colors"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"clean-webpack-plugin": "^0.1.16",
"css-loader": "^0.28.7",
"html-webpack-plugin": "^2.30.1",
"react-hot-loader": "^1.3.1",
"style-loader": "^0.18.2",
"uglifyjs-webpack-plugin": "^0.4.6",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.8.2",
"webpack-merge": "^4.1.0"
},
"dependencies": {
"react": "^15.6.1",
"react-dom": "^15.6.1"
}
}

As you can see, the scripts added in to the package.json are pretty much the same as we just tested. We added in “ — progress”and “ — colors” to get updates as the build is being created.

Give it a save and run:

npm start

Open up your browser to “http://localhost:8080” and try saving an update to your index.js file…

You should see that the build is recreated and your browser updates with the changes without refreshing the page… nice!

You can run a build and push to production server however you want using:

npm build

You are now ready to move on building your app without “create-react-app”. Happy Hacking!

--

--

Freelance Fullstack Blockchain Web Developer: I build projects using Solidity, Truffle, Node, React etc. Personal site: https://codylamson.com