This article will guide you through the process of creating a React app from scratch. It is meant for developers who want a better understanding of how tools like Babel, Webpack, DevServer, React, loaders, and presets make up a modern React app. We'll try to understand how they all fit together by incrementally piecing together an application.
The article was updated in 2022 for Webpack 5 and Babel 7.
Bootstrapping your own React app from scratch can be confusing. You don't simply run React in a browser. You need to bundle the different parts of your application together and transform your code into something the browser can understand.
Luckily there are some great tools that help you with that. You've probably heard of Webpack, a popular module bundler, and Babel, a tool that compiles "next generation" JavaScript into a backwards compatible version.
You could start your project by copying boilerplate code from a blog post or a Github repository. But I would suggest you do one of the following instead:
- Create your own boilerplate code and try to understand each step of the configuration process.
- Use create-react-app, a popular tool that lets you set up a React app with just one command. You don't need to get your hands dirty with Webpack or Babel because everything is preconfigured and hidden away from you.
While create-react-app is a great tool, the purpose of this article is to create our own boilerplate.
Let's jump right in.
Initialize the project
(This article has a companion Git repository: github.com/nicoqh/react-boilerplate)
We'll start by creating a basic directory structure for our project. We need a
directory for our source files which we'll call src
. We also need a directory
for compiled assets like JavaScript and HTML files. We'll call this directory
dist
.
mkdir src
mkdir dist
The next step is to create a .gitignore
file with the following content:
dist
node_modules
This will instruct Git to ignore the node_modules
directory and make sure we
don't accidentally commit every Node module we use in our project. We also want
to ignore the dist
directory. Everything inside this directory will be
compiled from the source files, so we don't need to add it to version control.
As with most Node-based projects, we need a package.json
that lists our
dependencies. Simply add an empty object for now:
{}
Let's add some code to our src
directory. Create the file index.js
with the
following code:
// src/index.js
const greet = (name) => console.log(`Hello, ${name}`);
greet("Jon Snow");
This code uses arrow functions and template literals which are ES6 features that don't yet work in every browser. The code needs to be transpiled. This brings us to Babel.
Babel
What is Babel?
According to its website, "Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments."
In other words we need Babel to transform modern JavaScript code into something browers can understand. We also need Babel to transform other "weird" stuff, like the JSX we will use with React, into something that makes sense to browsers.
To use Babel we need the compiler core and the Babel command line:
npm install --save-dev @babel/core @babel/cli
Now that Babel is installed in our node_modules
folder, let's try to run it on
our source and output the result to dist
:
./node_modules/.bin/babel src --out-dir dist
Did anything happen? If you look inside the dist
folder you'll see that our
code hasn't changed at all. This is because we haven't told Babel what to do.
Out of the box, Babel simply parses our code and returns it untouched.
Note: Instead of running the Babel executable located at
./node_modules/.bin/babel
, we can use a tool called
npx. npx makes it easier to execute local
packages. Usage: npx <command>
.
Syntax transformations
For Babel to do anything useful we need to enable some plugins. Plugins are
responsible for transforming our code and parsing/understanding the syntax. For
example, there's a plugin called @babel/plugin-transform-arrow-functions
which
transforms arrow functions to plain old JavaScript functions. However, instead
of installing a bunch of individual plugins (one for each transformation), Babel
offers something called "presets". You can think of a preset as an array of
plugins used to support a particular JavaScript language feature.
One of the official presets is called @babel/preset-env
. This is a "smart"
preset that allows us to use the latest JavaScript without needing to manage
which specific syntax transformations or polyfills are needed by your target
environments. (A "target environment" is an environment on which we want our
code to run, e.g. Chrome 99.) The preset lets us specify a set of environments,
and it will generate a list of plugins which it passes to Babel. With this list
of plugins Babel will only transform language features that are not
implemented in the browsers we target. That leads to a smaller bundle and less
code to parse.
Let's install @babel/preset-env
:
npm install --save-dev @babel/preset-env
Now, how do we specify our target environments?
@babel/preset-env
integrates with Browserslist,
a project that lets us specify target environments using queries. Babel
recommends putting the queries in a .browserslistrc
file.
Let's create the .browserslistrc
file:
# Browsers we support
> 0.5%
last 2 versions
not dead
These queries will select the last 2 versions of every browser that is not "dead" and has a market share above 0.5%. You can read more about Browserslist's query syntax at github.com/browserslist/browserslist
Let's run Babel again using the preset @babel/preset-env
:
npx babel src --out-dir dist --presets=@babel/preset-env
Check out the dist
folder—our code has been transformed! (Unless your target
environments support both arrow functions and template literals.)
"use strict";
var greet = function greet(name) {
return console.log("Hello, ".concat(name));
};
greet("Jon Snow");
Based on our Browserslist queries, @babel/preset-env
applied the necessary
transform plugins (in this case @babel/plugin-transform-arrow-functions
and
@babel/plugin-transform-template-literals
) to transform our code. If these
language features were already supported by the target environments, Babel
would have left the code untouched.
The Babel command will soon become cumbersome to manage (and to remember). Luckily Babel lets us specify our options in a configuration file.
Let's create the file babel.config.js
and configure Babel to use
@babel/preset-env
so we don't have to specify it on the command line:
// babel.config.js
const presets = [["@babel/preset-env"]];
const plugins = [];
// Export a config object.
module.exports = { presets, plugins };
Babel will detect this file automatically. We no longer need to manually type out our presets (or other configuration values) on the command line.
Let's try it:
npx babel src --out-dir dist
This should yield the same result as earlier.
It would be nice if we could see what plugins and presets were actually applied.
preset-env
takes a debug
option which, if set to true
, will instruct Babel
to output the targets and plugins it uses during compilation. Update your
babel.config.js
to look like this (view commit):
// babel.config.js
const presets = [
[
"@babel/preset-env",
{
// Pass a config object to the preset
debug: true, // Output the targets/plugins used when compiling
},
],
];
const plugins = [];
// Export a config object.
module.exports = { presets, plugins };
Run Babel again and the output will be similar to this:
// npx babel src --out-dir dist
@babel/preset-env: `DEBUG` option
Using targets:
{
"android": "98",
"chrome": "96",
"edge": "97",
"firefox": "96",
// ...
}
Using modules transform: auto
Using plugins:
proposal-class-static-block { ie, ios, safari, samsung }
proposal-private-property-in-object { ie, ios < 15, safari < 15, samsung }
proposal-class-properties { ie, ios < 15 }
Polyfills
Certain modern language features can be "transformed" into older syntax. This is the case with the arrow function above; it can be replaced by a plain old JavaScript function. Other functionality needs to be added to the runtime as "polyfills". For this we use a library called core-js.
core-js is a standard library for JavaScript that includes polyfills for a wide array of JavaScript features. The library lets us use features like promises and symbols in browsers that don't yet support them. By including a polyfill for a language feature, we can use the feature as if it were natively supported by the browser.
Install core-js:
npm install --save core-js
Import core-js at the top of your src/index.js
:
// src/index.js
import "core-js/stable"; // Loads all language features
// ... the rest of our code
We're now including every polyfill that is offered by core-js. Is this
necessary? We don't want to polyfill features that are already supported by our
target environments. As mentioned earlier, @babel/preset-env
uses Browserslist
to include only the transformation plugins we need. @babel/preset-env
can also
decide what polyfills to include from core-js using the preset's
useBuiltIns
option.
The useBuiltins
option takes one of the following values:
'entry'
: This will enable a plugin that transforms the import of core-js (import 'core-js/stable'
, like we did above), to imports of individualcore-js
polyfills. Our target environments will determine which polyfills to import. It doesn't matter if our app uses the language feature or not; as long as the feature is missing from one of our target environments, the polyfill is loaded.'usage'
: This option will add individual polyfill imports whenever a language feature is actually used in our source files. We don't need to manually import anything. Whenever we use a feature that isn't supported by one of our target environments, a polyfill is imported in the file that needs it.
Let's use the 'usage'
option so we don't have to worry about importing
polyfills.
Update your babel.config.js
(view commit):
// babel.config.js
const presets = [
[
"@babel/preset-env",
{
// Pass a config object to the preset
debug: true, // Output the targets/plugins used when compiling
// NEW CODE:
// Configure how @babel/preset-env handles polyfills from core-js.
// https://babeljs.io/docs/en/babel-preset-env
useBuiltIns: "usage",
// Specify the core-js version. Must match the version in package.json
corejs: 3,
// Specify which environments we support/target. We have chosen to specify
// targets in .browserslistrc, so there is no need to do it here.
// targets: "",
// END NEW CODE
},
],
];
const plugins = [];
// Export a config object.
module.exports = { presets, plugins };
We also need to tell Babel which core-js version we're using. @babel/preset-env
supports both version 2 and 3. We have specified the version by setting
corejs: 3
. This should match the version specified in your package.json
.
Because we have set useBuiltIns: 'usage'
we can remove the
import 'core-js/stable'
statement from src/index.js
. As you may remember,
the 'usage'
option takes care of importing any necessary polyfills.
Our current configuration will have the following effect on our code:
// Before transformation
var a = new Promise();
// After transformation (if the environment doesn't support promises):
import "core-js/modules/es.promise"; // The promise polyfill is imported ...
var a = new Promise(); // ... so this will work.
(Side note: Are you wondering why we're not using @babel/polyfill
? This
package has been deprecated in favor of importing core-js
like we did above.)
That's it for Babel. Let's continue to Webpack.
Webpack
What is Webpack?
According to its website, Webpack is "a static module bundler for modern JavaScript applications". Webpack creates a graph of every module our app uses (JavaScript files, React components, images, CSS files etc.), and generates one or more bundles. It's not uncommon to generate one bundle that contains all the modules that make up an application.
Let's install Webpack and its command line tool (CLI):
npm install --save-dev webpack webpack-cli
Create the file webpack.config.js
and add the following content (view
commit):
// webpack.config.js
const path = require("path");
// We'll refer to our source and dist paths frequently, so let's store them here
const PATH_SOURCE = path.join(__dirname, "./src");
const PATH_DIST = path.join(__dirname, "./dist");
// Export a configuration object
module.exports = {
// Tell Webpack to do some optimizations for our environment (development
// or production). Webpack will enable certain plugins and set
// `process.env.NODE_ENV` according to the environment we specify.
// https://webpack.js.org/configuration/mode
mode: "development",
// The point or points to enter the application. This is where Webpack will
// start. We generally have one entry point per HTML page. For single-page
// applications, this means one entry point. For traditional multi-page apps,
// we may have multiple entry points.
// https://webpack.js.org/concepts#entry
entry: [path.join(PATH_SOURCE, "./index.js")],
// Tell Webpack where to emit the bundles it creates and how to name them.
// https://webpack.js.org/concepts#output
// https://webpack.js.org/configuration/output
// https://webpack.js.org/configuration/output#outputFilename
output: {
path: PATH_DIST,
filename: "js/[name].[contenthash].js",
// The public URL of the output dir when referenced in a browser.
// This value is prefixed to every URL created by the runtime or loaders.
// It's empty by default, which creates URLs like 'bundle.js' and results
// in 404s if they're requested from a nested URL like /articles/1
// https://webpack.js.org/configuration/output/#outputpublicpath
publicPath: "/",
},
};
This is a pretty basic Webpack configuration file. You should read through the comments to get a sense of its structure.
Our entry point is ./src/index.js
and our compiled bundle will be emitted to
./dist/js/[name].[contenthash].js
. Webpack will substitute [name]
with the entry
name, which is main
by default. [contenthash]
will be replaced with a hash
of the module's (the file's) content. This is great for HTTP caching. We can
tell browsers to cache JavaScript files aggressively. Whenever we re-build the
app with updated code, the bundle name (the hash) changes as well. This will
break the browser's cache and force it to re-download the bundle.
Because we only have one entry point and only create one bundle, the output file
could have a static name, like bundle.js
. But we'll keep it dynamic in case we
need multiple bundles later, and because [contenthash]
is useful for caching.
Before we run Webpack with our newly created config, let's create an npm script so we don't need to type out the whole command.
Open your package.json
and add the "scripts"
section (view commit):
{
"scripts": {
"dev": "webpack --config webpack.config.js"
},
"devDependencies": {
// ... the rest of package.json
Now we can simply run:
npm run dev
If we take a look in our dist
directory we'll find a js
directory with a
file named main.[some-hash].js
. This is our bundle. It contains a lot of
Webpack-specific code which we don't need to care about. Somewhere at the bottom
you'll also see our application code.
A new bundle will be generated every time we change the source code and run the
npm script npm run dev
. Eventually the directory will be littered with old
bundles. We'll deal with this nuisance later. For now you can simply delete the
dist
folder regularly.
If you look closely in dist/js/main.[some-hash].js
you'll notice that our code
hasn't been transformed and that no polyfills have been loaded. This is because
we haven't told Webpack to use Babel yet. We only ran the Webpack command, with
no mention of Babel. We'll fix that soon, but first we'll create a
module to see how Webpack handles
so-called "bundling".
Create a file named sum.js
in the source directory with the following contents:
// src/sum.js
const sum = (a, b) => a + b;
export default sum;
This is our first module. It consists of an arrow function that returns the sum
of two numbers, a
and b
. This module can be imported from anywhere.
Let's use sum
in src/index.js
:
// src/index.js
import sum from "./sum";
console.log(sum(2, 4)); // Output: 6
Run Webpack again.
npm run dev
Just like before, our bundle ends up in dist/js/main.[some-hash].js
. It
contains all our code, including the imported sum
module. Let's run our bundle
with Node to see if it works:
node dist/js/main.[some-hash].js
You should see 6
on the command line.
Building the application with Webpack works as expected, but we still need to include Babel in our build process. This brings us to a Webpack concept called "loaders".
Webpack Loaders
We have already configured Babel to run transformations on our code and import any necessary polyfills. But, at the moment, we're not running Babel, we're only running Webpack. Our next task is to tell Webpack how to include Babel in its build process. This is achieved with "loaders".
Loaders are used to tell Webpack how to treat the different modules that we
import throughout our app. Using loaders we can tell Webpack what to do when we
import sum from './sum'
, import './styles/main.scss'
and
import logo from './logo.png'
.
For example, using a loader we can instruct Webpack to run .scss
files through a Sass
compiler.
In other words, loaders are transformations that are applied on the source code of a module. They allow us to pre-process files as we import or "load" them.
We want Webpack to run Babel on all our JavaScript modules. Whenever we import a
file that ends in .js
we want Webpack to use a Babel "loader". The Babel
loader will run Babel on the imported code, and Babel will transform it
according to our Babel configuration.
There's a loader conveniently called babel-loader
which we'll install:
npm install --save-dev babel-loader
Next we'll update our Webpack configuration. We'll create a new section called
modules
in which we'll specify how our modules should be treated by Webpack.
In this section we will add some rules
which tell Webpack how and when to use
the loaders.
Update your webpack.config.js
to reflect the following changes (view
commit):
// webpack.config.js
// ...
output: {
path: PATH_DIST,
filename: 'js/[name].[hash].js',
},
// NEW CODE:
// Determine how the different types of modules will be treated.
// https://webpack.js.org/configuration/module
// https://webpack.js.org/concepts#loaders
module: {
rules: [
{
test: /\.js$/, // Apply this rule to files ending in .js
exclude: /node_modules/, // Don't apply to files residing in node_modules
use: { // Use the following loader and options
loader: "babel-loader",
// We can pass options to both babel-loader and Babel. This option object
// will replace babel.config.js
options: {
presets: [
["@babel/preset-env", {
debug: true, // Output the targets/plugins used when compiling
// Configure how @babel/preset-env handles polyfills from core-js.
// https://babeljs.io/docs/en/babel-preset-env
useBuiltIns: "usage",
// Specify the core-js version. Must match the version in package.json
corejs: 3,
// Specify which environments we support/target for our project.
// (We have chosen to specify targets in .browserslistrc, so there
// is no need to do it here.)
// targets: "",
}],
],
},
}
}
],
},
// END NEW CODE
};
You should read the comments as they explain most of what's going on.
Notice the options
object that we pass to babel-loader
. This object has been
copied directly from our babel.config.js
. Instead of having Babel read
babel.config.js
, we can pass our options through Webpack. As you'll see
later, it's very convenient to put our Babel configuration inside our Webpack
configuration.
You can safely delete babel.config.js
.
Test the new Webpack config by running the npm script we created earlier:
npm run dev
In summary, Webpack bundles our code. It also uses the "loader" babel-loader
to run Babel on any file (module) that ends in .js
. Babel transforms our code
to a backward-compatible version.
Development vs production
Eventually we want to differentiate development builds from production builds.
Our Webpack config currently exports a configuration object. If we instead export a function, Webpack will invoke it with an environment as the first argument. We can use this argument to include or exclude configuration options based on whether we're building for production or development.
Consider this example:
// Exporting an object from webpack.config.js:
// (This is what we're currently doing.)
module.exports = {
// Our Webpack config object.
};
// If we export a function, it will be passed two parameters, the first
// of which is the webpack command line environment option `--env`.
// `webpack --env a=b` sets env.a = 'b'
// `webpack --env environment=production` sets env.environment = 'production'
// https://webpack.js.org/configuration/configuration-types/#exporting-a-function
module.exports = (env) => {
// Use the `env` argument to create some helpful constants.
const environment = env.environment;
const isProduction = environment === "production";
const isDevelopment = environment === "development";
return {
// Our Webpack config object.
// We now have access to the constants `environment`,
// `isProduction` and `isDevelopment`.
mode: environment,
};
};
Let's incorporate this into our Webpack config. Our new webpack.config.js
should look like this (view commit):
// webpack.config.js
const path = require("path");
// We'll refer to our source and dist paths frequently, so let's store them here
const PATH_SOURCE = path.join(__dirname, "./src");
const PATH_DIST = path.join(__dirname, "./dist");
// If we export a function, it will be passed two parameters, the first
// of which is the webpack command line environment option `--env`.
// `webpack --env.a = b` sets env.a = 'b'
// `webpack --env.production` sets env.production = true
// https://webpack.js.org/configuration/configuration-types/#exporting-a-function
module.exports = (env) => {
const environment = env.environment;
const isProduction = environment === "production";
const isDevelopment = environment === "development";
return {
// Tell Webpack to do some optimizations for our environment (development
// or production). Webpack will enable certain plugins and set
// `process.env.NODE_ENV` according to the environment we specify.
// https://webpack.js.org/configuration/mode
mode: environment,
// The point or points to enter the application. This is where Webpack will
// start. We generally have one entry point per HTML page. For single-page
// applications, this means one entry point. For traditional multi-page apps,
// we may have multiple entry points.
// https://webpack.js.org/concepts#entry
entry: [path.join(PATH_SOURCE, "./index.js")],
// Tell Webpack where to emit the bundles it creates and how to name them.
// https://webpack.js.org/concepts#output
// https://webpack.js.org/configuration/output
// https://webpack.js.org/configuration/output#outputFilename
output: {
path: PATH_DIST,
filename: "js/[name].[contenthash].js",
},
// Determine how the different types of modules will be treated.
// https://webpack.js.org/configuration/module
// https://webpack.js.org/concepts#loaders
module: {
rules: [
{
test: /\.js$/, // Apply this rule to files ending in .js
exclude: /node_modules/, // Don't apply to files residing in node_modules
use: {
// Use the following loader and options
loader: "babel-loader",
// We can pass options to both babel-loader and Babel. This option object
// will replace babel.config.js
options: {
presets: [
[
"@babel/preset-env",
{
debug: true, // Output the targets/plugins used when compiling
// Configure how @babel/preset-env handles polyfills from core-js.
// https://babeljs.io/docs/en/babel-preset-env
useBuiltIns: "usage",
// Specify the core-js version. Must match the version in package.json
corejs: 3,
// Specify which environments we support/target for our project.
// (We have chosen to specify targets in .browserslistrc, so there
// is no need to do it here.)
// targets: "",
},
],
],
},
},
},
],
},
};
};
These changes will come in handy when we add more options and loaders later. But
how do we pass the environment to Webpack? With the --env
option:
webpack --env environment=production --config webpack.config.js
We can now create two different npm scripts, one for each environment. Change the
"scripts" section of your package.json
to look like this:
// package.json
{
"scripts": {
"build": "webpack --env.environment=production --config webpack.config.js",
"dev": "webpack --env.environment=development --config webpack.config.js"
},
// ...
To create a production build we simply run:
npm run build
And for development builds:
npm run dev
Adding index.html (HtmlWebpackPlugin)
This is our current directory structure:
├── src
│ ├── index.js
│ └── sum.js
├── dist
│ └── js
│ └── main.[some-hash].js
├── package.json
├── package-lock.json
├── webpack.config.js
└── .browserslistrc
Eventually we want to deploy the dist
directory to a server, but first we need
an index.html
which will serve as the entry point to our web application.
The HTML file should import the bundle, like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Boilerplate!</title>
</head>
<body>
<script src="/js/main.c9eb7e60a479f1a2d6bc.js"></script>
</body>
</html>
However, instead of putting a static HTML file inside dist
, we want Webpack to
generate the file automatically. There are a few reasons for this:
- We don't want to hardcode the bundle's file name which changes frequently.
- We've told Git to ignore the
dist
directory (using.gitignore
), so everything we put inside it will be lost. - We may eventually want to include other dynamic content, like placeholders, in
index.html
.
There's a Webpack plugin called HtmlWebpackPlugin that will generate HTML files for us. Let's install it:
npm install --save-dev html-webpack-plugin
First we need an HTML template for HtmlWebpackPlugin to use. Create index.html
and place it in the src
directory:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Boilerplate!</title>
</head>
<body></body>
</html>
This file will be used as a basis for the generated index.html
, so feel free
to add your own markup, like meta tags or Open Graph tags.
The <script>
tag is omitted because it will be added by HtmlWebpackPlugin
automatically.
To use the plugin we need to import it at the top of webpack.config.js
. We
also need to enable it by adding it to the plugins
array of the Webpack
configuration object. Since this is the first plugin we add, we need to create
the plugins
array (view commit):
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // NEW LINE
// ...
module: {
// ...
},
// NEW CODE:
plugins: [
// This plugin will generate an HTML5 file that imports all our Webpack
// bundles using <script> tags. The file will be placed in `output.path`.
// https://github.com/jantimon/html-webpack-plugin
new HtmlWebpackPlugin({
template: path.join(PATH_SOURCE, './index.html'),
}),
],
// END NEW CODE
};
};
(Notice the import of HtmlWebpackPlugin at the top.)
You can test the changes by creating a new build:
npm run build
The automatically generated index.html
has been added to dist/index.html
.
Cleaning out the dist directory
You may have noticed that the dist
directory has started to fill up with old
bundles. Let's remedy this by installing a Webpack plugin that automatically
cleans out the directory before every new build.
Install clean-webpack-plugin
:
npm install --save-dev clean-webpack-plugin
Then, import the plugin at the top of webpack.config.js
and add it to the
plugins
array (view commit):
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // NEW LINE
// ...
plugins: [
// ...
// NEW CODE:
// This plugin will delete all files inside `output.path` (the dist directory),
// but the directory itself will be kept.
// https://github.com/johnagan/clean-webpack-plugin
new CleanWebpackPlugin(),
// END NEW CODE
],
};
};
Webpack DevServer
Let's do a quick recap. Our bundle is emitted to the dist
directory. The
directory also contains a generated index.html
file which references our
bundle. The dist
directory is actually ready to be deployed.
But there's one thing that quickly becomes annoying. We need to run Webpack each
time we change our code. Is there a way to run our npm script, npm run dev
,
automatically whenever our code changes?
One way is by using Webpack's "watch mode". Adding --watch
to the Webpack
command will instruct Webpack to "watch" the source files and recompile whenever
there is a change. But there's a more powerful option: Webpack's
DevServer
The DevServer is a simple web server that serves content from the dist
directory. Besides recompiling your bundle automatically, it will "live reload"
your browser whenever your code recompiles.
Install DevServer:
npm install --save-dev webpack-dev-server
Add the devServer
option to our Webpack configuration object
(view commit):
// webpack.config.js
// ...
'mode': environment,
// NEW CODE:
// Configuration options for Webpack DevServer, an Express web server that
// aids with development and provides live reloading out of the box.
devServer: {
static: {
// The dev server will serve content from this directory.
directory: PATH_DIST,
},
// Specify a host and port number.
host: "localhost",
port: 8080,
// When using the HTML5 History API (you'll probably do this with React
// later), index.html should be served in place of 404 responses.
historyApiFallback: true,
client: {
// Show a full-screen overlay in the browser when there are compiler
// errors or warnings.
overlay: {
errors: true,
warnings: true,
},
},
},
// END NEW CODE
// ...
DevServer doesn't write any files after compiling. It won't write anything to
dist
. It keeps bundle files in memory and serves them as if they were real
files mounted at the server's root path. For example, <script src="js/main.js">
will trigger a request to js/main.js, which will serve the contents of js/main.js
from memory.
Add an npm script for the DevServer (view commit):
// package.json
{
"scripts": {
"build": "webpack --env.environment=production --config webpack.config.js",
"dev": "webpack --env.environment=development --config webpack.config.js",
"devserver": "webpack-dev-server --env.environment=development --config webpack.config.js"
},
// ...
And run it:
npm run devserver
This will fire up a web server on http://localhost:8080
. You can visit the URL
and check out your browser's development console. You should see the message we
wrote with console.log
.
Let's recap our three npm scripts:
npm run dev
will create and emit a development bundle to thedist
folder.npm run prod
will create and emit a production bundle to thedist
folder.npm run devserver
will fire up Webpack's DevServer which creates a development bundle, stores it in memory, and serves it. It will also recompile and "live reload" whenever the code changes.
React
We need two packages to use React. First we need the
generic React package (react
). We also need react-dom
which takes care of
DOM-specific operations
like rendering our application on the web platform.
npm install --save react react-dom
React components are typically written using JSX, a syntax extension to JavaScript. We won't learn JSX in this article, but you should know that this code:
const element = <h1>Hello, world!</h1>;
is a developer-friendly way of writing:
const element = jsx("h1", null, "Hello, world!");
(Why not React.createElement()
? Because we're using the new JSX
transform.)
Browsers don't understand JSX so we need to transform it to calls to
jsx()
, and import the jsx
function.
There's a Babel preset that will do this for us: @babel/preset-react
.
This preset includes several plugins that are required to write a React app. It
helps Babel understand the JSX syntax and converts our JSX to jsx()
calls.
npm install --save-dev @babel/preset-react
Open webpack.config.js
and add @babel/preset-react
to the presets
array
under the rule for babel-loader
(view commit):
// ...
options: {
presets: [
[
"@babel/preset-env",
{
// ...
},
],
// NEW CODE:
// The react preset includes plugins that are required for React
// apps. For example, it inclues a plugin that transforms JSX.
[
"@babel/preset-react",
{
// Tell "plugin-transform-react-jsx" which runtime to use.
// The "automatic" runtime will:
// * Import the jsx() function in your JSX files:
// `import { jsx as _jsx } from "react";`
// * Transform JSX: `<div />` to `_jsx("div")`
runtime: "automatic",
},
],
// END NEW CODE
];
}
// ...
Now that Babel is ready to work with JSX, we can create our first React component
and render it to the DOM. Replace the contents of src/index.js
with this
(view commit):
import React from "react";
import ReactDOM from "react-dom";
function Root() {
return <h1>Hello, world.</h1>;
}
// Render the Root element into the DOM
ReactDOM.render(<Root />, document.getElementById("root"));
The call to ReactDOM.render()
will render the React component <Root>
into
the supplied container. Our container is an HTML element with an ID of root
.
This element needs to exist in our HTML template, so let's add it to src/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Boilerplate!</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
That's it. Fire up Webpack's DevServer, hit http://localhost:8080
and enjoy
your exciting new app.
Where to go from here
The ecosystem is vast and your boilerplate will likely grow in complexity as you customize it further and add more plugins and presets. Maintaining your own boilerplate gives you a lot of flexibility, but it can also become frustrating to keep up with all the tools. If you don't think it's worth the effort, there's always create-react-app.
If you want to flesh out your boilerplate code, here are some tips on what to explore next:
- Install the React DevTools.
- Set up Hot Module Replacement
- Tooling for CSS. Either a CSS-in-JS solution like Emotion, or a
combination of
style-loader
,css-loader
and a pre-processor loader likesass-loader
if you want to keep your CSS away from your JS. - Check out copy-webpack-plugin.
Put public files like favicons in a dedicated directory (e.g.
src/public
) and use this plugin to copy the directory's contents todist
. - Are you using PropTypes? Install babel-plugin-transform-react-remove-prop-types if you want to remove them in production.
A future article will likely cover some of these topics.
Read part two: Create a React app from scratch - ESLint and Prettier.