0 to 100 with Webpack and React

I’ve been working on a React-based project for about a year now. Our project’s build process is powered by Webpack. It feels like that’s a pretty common scenario these days. Even the State Of JavaScript 2017 Survey shows that a lot of people are using Webpack for their build tool. But even with those stats, I think Webpack has a steep learning curve.

This tutorial aims to help spread our understanding with a no-frills walkthrough from scratch to illustrate what Webpack does in your project.

Step by Step

Initial project setup

This article assumes a minimal familiarity with npm but uses the yarn client. The npm docs can get you up to speed. Install yarn or substitute the npm command.

From your terminal, create a new directory and navigate into it. We will be using yarn for this tutorial but npm works the same. Initialize the project by running yarn init -y.

> mkdir floral-thunder && cd floral-thunder
> yarn init -y

When that’s complete, you’ll have a package.json file and be ready to go.

Getting started with Webpack

> yarn add --dev webpack

If you’re using yarn or the latest version of npm, you’ll notice a yarn.lock or package-lock.json file was added. Don’t worry about it right now; just know it will help you in the future §.

If you are using git with your project, now’s a great time to add the ‘node_modules’ directory to your .gitignore.

With Webpack installed, we can define our configuration. Create a new file called ./webpack.config.js, open it in your editor of choice, and add the following:

// ./webpack.config.js

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

We won’t be going into heavy detail about Webpack configuration, I’ll let the improved documentation speak for itself. That said, we are interested in just two keys in our config right now; entry and output. We’ve set entry to the most basic config, just a path to the root of our dependency graph or entry file. output is also set to a very basic configuration; the filename template for Webpack’s output and the path to where it should be written to.

Before we can test that our configuration works, let’s create the ./src and ./dist directories and add an empty ./src/index.js file. Now you can yarn run webpack and Webpack will use our config (by filename convention) and you should see a some output about our bundle in the terminal along with a newly created file ./dist/main.bundle.js. “main” is a Webpack default and you can see Webpack used our filename template for output.

Congrats! You just used Webpack to build a bundle.

Speeding up development with webpack-dev-server

Typing yarn run webpack every time we want to compile our project will get old really fast. Webpack has a watchoption, but you also want to have our files served up on a port of our choosing because there are some things that don’t work with file: URLs in web browsers. webpack-dev-server has the same watch option enabled by default and it allows us to serve up a directory via HTTP. So, let’s install it.

> yarn add --dev webpack-dev-server

Now we can add some bits to our webpack.config.js and try it out.

 // ./webpack.config.js

 const path = require('path');
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist')
+  },
+  devServer: {
+    contentBase: path.resolve(__dirname, 'dist')
   }
 };

Now, instead of running yarn run webpack, let’s use yarn run webpack-dev-server. You’ll see similar output to the previous command but now we see “Project is running at http://localhost:8080”. With webpack-dev-server, our bundle is served up via a simple web server and our source files are being watched; making a change will trigger a rebuild of our bundle. Visiting the provided URL will also show that we aren’t serving an HTML document; we just see a file manifest. You may also notice that the Webpack bundle itself is larger. This is due to additional tooling injected from webpack-dev-server. Production builds won’t suffer from this bloat.

Changes to your Webpack configuration or other tooling changes will require a restart of the development server.

The next step is to add an HTML document that we can mount on. We could add something like the HtmlWebpackPlugin, but lets keep things dead simple. Add an index.html file to the ./dist directory and add the following HTML.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="./main.bundle.js" async></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

It might already be obvious to you, but our index.html won’t benefit from our live reloading because it’s a static asset, not generated by Webpack. Back in your terminal hit ctrl + c to stop Webpack and enter yarn run webpack to restart the build. Let’s add something to our entry file to make sure this is all working. In ./src/index.js, add a simple console.log('Hello World');.

Now if we visit our page in the browser and reload, we should see our log in the browser console!

To recap, we’ve setup a new project, added webpack and the webpack-dev-server, written a minimal Webpack config, and used webpack-dev-server to bundle and serve our files in development. Next step is to add React.

Adding React

First, we’ll need to add the React dependencies to our project:

> yarn add react react-dom

Since React can be used for many targets and we are build for the web, we need both react and the react-dom libraries. With those installed we can set up a very simple example. After starting your development server with yarn run webpack-dev-server, open your ./src/index.js and update it with the following:

// ./src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
const mountNode = document.getElementById('root');
class HelloMessage extends React.Component {
  render() {
    return React.createElement("div", null, "Hello ", this.props.name);
  }
}
ReactDOM.render(React.createElement(HelloMessage, { name: "Jane" }), mountNode);

Visiting our page in the browser should show “Hello Jane”.

Let’s break this file down a bit. First, we import the dependencies we need. Next, we store the element we plan to mount to and store it as mountNode. Then, we define a React component by inheriting from React.Component. This is just one way of defining a component. Then, on the last line, we use ReactDOM.render to render the component we just made to the mountNode while passing a name prop.

As you may be aware, React optionally uses an XML-like syntax called JSX. In the above example, we didn’t use JSX, but used what JSX compiles to. To support JSX, we also need to use a tool called Babel. We’ll set that up in a later section.

Extract a simple component

From here, we could easily start building or using more components. In fact, let’s move our HelloMessage component into its own file in ./src/HelloMessage.js. We have to import React in this component file as well, and we’ll need to export the component to ensure we can consume it elsewhere:

// ./src/HelloMessage.js

import React from 'react';
class HelloMessage extends React.Component {
  render() {
    return React.createElement("div", null, "Hello ", this.props.name);
  }
}
export default HelloMessage;

Then in ./src/index.js we define a new App component and import then use our HelloMessage component a few times. Don’t forget to update the ReactDOM.render call to use the new App component.

// ./src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import HelloMessage from './HelloMessage';
const mountNode = document.getElementById('root');
class App extends React.Component {
  render() {
    return React.createElement(
      "div",
      null,
      React.createElement(HelloMessage, {name: 'Aaron'}),
      React.createElement(HelloMessage, {name: 'John'}),
      React.createElement(HelloMessage, {name: 'Jane'})
    );
  }
}
ReactDOM.render(React.createElement(App), mountNode);

Our webpack-dev-server should be seeing our file changes and reloading our browser for us this entire time. As you can see, rendering many children or a more nested hierarchy of components can become pretty verbose without JSX, but we did it! We have rendered a wrapping div and 3 of our HelloMessage components with different props!

We covered quite a bit, and hopefully it’s given you a better understanding of what goes into setting up a Webpack project with React.

Now, let’s go just a little bit further and add JSX and CSS Module support, so we can build our app faster and make it pretty!

Adding JSX support

First up, JSX support! To set this up, we will be learning another Webpack concept – Rules (formerly known as Loaders)! The terminology and API around this concept has changed a bit but at its core, it’s still the same. By default, Webpack takes the files you specify via configuration and recursively builds a dependency graph and packages those into a smaller number of bundles (typically one). During this process, Webpack allows us to specify how to handle each type of module in our application. What we want to do is specify some rules that will match our files with JSX, and run a specific transformation across the code, transpilation of JSX to the corresponding React calls.

The first step is to install some dependencies. We need two things to start; babel-core and babel-loader for use with Webpack. yarn add --dev babel-core babel-loader

You might be worried about all these dependencies we’re adding, but everything except react and react-dom so far have been devDependencies. These new dependencies will also be for development only.

With those dependencies, we can setup Babel with our Webpack config. Below the devServer property in our webpack.config.js add the following:

 // ./webpack.config.js

 const path = require('path');
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist')
   },
   devServer: {
     contentBase: path.resolve(__dirname, 'dist')
+  },
+  module: {
+    rules: [
+      { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }
+    ]
   }
 };

Poof! That’s it, we’ve configured babel to transform our JavaScripts!

Let’s break down what we did. Webpack allows you to have many rule’s, each describing how a module can be modified during transformation. rule’s are comprised of a condition, loaders, parser options, and potentially nested rules.

With babel setup, we can now easily add transformations via plugins and presets (collections of plugins). We want a JSX transformation, so we don’t have to use React.createElement for everything. There is an official Babel preset for React, so let’s install and configure that. yarn add --dev babel-preset-react then we can instruct Babel to use the preset. One of the easiest (and recommended) ways to use presets or a plugins is via a ./.babelrc file. Running echo '{ "presets": ["react"] }' > .babelrc will do just that.

Now we can replace our React.createElement calls with JSX! Check out the changes to our ./src/index.js below:

 // ./src/index.js

 import React from 'react';
 import ReactDOM from 'react-dom';
 import HelloMessage from './HelloMessage';
 const mountNode = document.getElementById('root');
 class App extends React.Component {
   render() {
-    return React.createElement(
-      "div",
-      null,
-      React.createElement(HelloMessage, {name: 'Aaron'}),
-      React.createElement(HelloMessage, {name: 'John'}),
-      React.createElement(HelloMessage, {name: 'Jane'})
+    return (
+      <div>
+        <HelloMessage name='Aaron' />
+        <HelloMessage name='John' />
+        <HelloMessage name='Jane' />
+      </div>
     );
   }
 }
-ReactDOM.render(React.createElement(App), mountNode);
+ReactDOM.render(<App />, mountNode);

Much cleaner! Not that there’s anything wrong with React.createElement. If you prefer that, feel free to use it! With JSX, our usage of components becomes a bit clearer and the relationship between components becomes a bit cleaner. At its core, JSX is syntactic sugar; powerful, expressive sugar. Learn more about JSX in the official React docs on JSX.

Adding CSS modules support

Now that we cleaned up our React code, let’s put the same Webpack knowledge to use to parse and transform CSS for us. Doing this will enable us to have scoped CSS selectors, meaning no more collisions or globals, and it will help us apply those selectors to our React elements. We are going to use two new (to us) loaders to help us do it. style-loader will inject our CSS into the document via a <style> tag, and the css-loader parses CSS files and can apply transforms to it; specifically class hashing.

Let’s install our dependencies by running yarn add --dev style-loader css-loader. Then let’s add to our Webpack configuration. Since our CSS will be another module in our dependency graph, we just need to add a few rule’s and Webpack will start transforming it for us. In the rules array, add the following:

 // ./webpack.config.js

 const path = require('path');
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist')
   },
   devServer: {
     contentBase: path.resolve(__dirname, 'dist')
   },
   module: {
     rules: [
-      { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ }
+      { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ },
+      { test: /\.css$/, loader: 'style-loader' },
+      { test: /\.css$/, loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]___[hash:base64:5]' } }
     ]
   }
 };

Lets unpack this a bit. The first new rule we added will match all our .css files and uses the style-loader to inject the styles into our HTML document. We will see this in action in just a bit. The next rule also matches all our .cssfiles but it uses css-loader and has some options. The css-loader has features like source maps, minification, and @import statements but the feature we’re using first is CSS modules. In the options property we have modules: true, which turns on CSS modules. Then we have localIdentName, which is a template the loader will use to namespace our CSS rules.

You can condense the two rules we defined in our Webpack config into one.

Let’s write some CSS! Start by creating a new file in called app.css in the ./src/ directory and then opening it in your editor. Let’s just add a single rule like the following:

/* ./src/app.js */

.app {
  border: 5px solid palevioletred;
}

Now open up ./src/index.js where we have our App component defined. First lets import our CSS at the top of the file so we can use it. Now we can apply our CSS via the style object that came from the import. In our Appcomponent we have a wrapper div that we want to add our CSS to. In React we use the className attribute to assign classes to our components so we can add it to the wrapping div.

 // ./src/index.js

 import React from 'react';
 import ReactDOM from 'react-dom';
 import HelloMessage from './HelloMessage';
+import style from './app.css';
 const mountNode = document.getElementById('root');
  class App extends React.Component {
    render() {
      return (
-       <div>
+       <div className={style.app}>
          <HelloMessage name='Aaron' />
          <HelloMessage name='John' />
          <HelloMessage name='Jane' />
        </div>
      );
    }
  }
 -ReactDOM.render(React.createElement(App), mountNode);
 +ReactDOM.render(<App />, mountNode);

Let’s see our work in action! Start or restart your dev server and visit your page in the browser. You should see the same components, but now there is a border around them. Open up the developer tools and take a peek at what’s in the <head> element. You should see something like the following just below your <script> tag:

<style type="text/css">
.app__app___2x3cr {
  border: 5px solid palevioletred;
}
</style>

Look at that! The css-loader has parsed our CSS and scoped our selectors resulting in transformed names. This means we can write CSS for each component without worrying about collisions with other selectors.

Wrap up

There’s loads more we can do to improve our developer experience, and tons of stuff we can do to prepare our application bundle for production, but we’ll save those topics for another article.

Thanks for reading, and I hope you’ve learned a few things about Webpack and React to give you a bit more confidence in how to use these tools to build killer apps!

Footnotes

§ yarn.lock and package-lock.json https://yarnpkg.com/lang/en/docs/yarn-lock/ and https://docs.npmjs.com/files/package-locks. [back]

HtmlWebpackPlugin https://webpack.js.org/plugins/html-webpack-plugin/. [back]

import with Webpack does a bit of transpilation for us here, converting the import statements into __webpack_require__ calls. https://webpack.js.org/guides/getting-started/#modules. [back]

Aaron Trostle
November 14th, 2017
Receive expert insights, tips + tricks every month
RELATED ARTICLES
Get our newsletter