React Tooling Part 2

In this section, we will discuss about webpack and the ecosystem around it.

webpack

webpack logo

Modern JS applications usually involved hundreds, if not thousands of files/functions. It would be painful and error-prone if we just include <script> tags in our html file manually.

This may not seems obvious to us now because currently we have only two components App and Movie. However, even with only two components, we need to bear in mind that React and ReactDOM are our dependencies and we need to ensure our code only run after they are loaded. Try to move our script tag above the React and ReactDOM script tags, and you realize our code no longer works because React code has not loaded yet.

Now imagine you have 50+ components and more than 20 dependencies, and there are some inter-dependencies between those files, manually analyzing and rearranging your script tags would be very painful, if not impossible.

webpack is the tools that solves this problem. It allows us to use JS module system like commonjs (e.g. const React = require('react')) and ECMAScript Module (e.g. import React from 'react') by parsing them and create a single bundled output file. On top of that, webpack has been extended to handle all types of files that involved in a modern web application, e.g. css, images, html etc.

Bundling code with webpack

To use webpack in your project:

  1. Install it in your project:

    bash
    npm install -D webpack webpack-cli
    bash
    npm install -D webpack webpack-cli
  2. Create a folder and call it src. Move your script.js into this folder and rename it as index.js.

  3. Add the following npm scripts:

    package.json
    json
    {
    "scripts": {
    //...
    "build": "webpack --mode=\"development\""
    }
    }
    package.json
    json
    {
    "scripts": {
    //...
    "build": "webpack --mode=\"development\""
    }
    }
  4. Run npm run build

  • webpack will produces a file call main.js in dist folder.
    • webpack will includes some additional code (known as webpackBootstrap). The code is the runtime of webpack to manage your dependencies and perform some magic e.g. lazy-loading (which we will explain later).
    • If you update your index.html to use script from dist/main.js, the code should works as before.

Now that webpack is processing our file, let’s utilize webpack by splitting App and Movie into their own module/file:

  1. Create a file alongside index.js and call it movie.js.
  2. Cut and paste the Movie component into movie.js.
  3. At the bottom of the movie.js, add the following statement:
    js
    export default Movie;
    js
    export default Movie;
  4. Create another file and call it app.js.
  5. Cut and paste the App component into app.js.
  6. At the top of the app.js, add the following statement:
    js
    import Movie from './movie';
    js
    import Movie from './movie';
  7. At the bottom of the app.js, add the following statement:
    js
    export default App;
    js
    export default App;
  8. All App and Movie code should be removed from index.js now. Add the following statement at the top of index.js:
    js
    import App from './app';
    js
    import App from './app';

Functionally, our code still works the same. However, now we organize our code with modules and dependencies between them are managed by webpack.

  • import statement is used to include code that the module depends on. For our example above:
    • app.js depends on movie.js
    • index.js depends on app.js
  • export statement is used to declare the code that is exposed to other modules.
    • there are two types of export statement, i.e. default export and named export.
    • We use default export in our files currently. You can read more about them here.

Exercise

  1. Install webpack and configure build script based on the instruction above.
  2. Verify that your code still works as before.
  3. (Bonus) use named exports for movie.js.

Commit: 030-webpack-app-code

Bundling external dependencies

Now webpack is managing dependencies between our codes (index.js, app.js and movie.js), let’s take one step further to utilize it to manage the our dependencies to React and ReactDOM.

  1. Install React and ReactDOM: npm install react react-dom. Note that there is no -D flag as we need React and ReactDOM in our application code, not tools during development/building.

  2. At the top of your index.js, add the following statements:

    src/index.js
    js
    import * as React from 'react';
    import ReactDOM from 'react-dom';
    src/index.js
    js
    import * as React from 'react';
    import ReactDOM from 'react-dom';
  3. At the top of app.js and movie.js, add the following statement:

    js
    import * as React from 'react';
    js
    import * as React from 'react';
  4. Remove the unpkg script tags for React and ReactDOM in your index.html.

  5. Run npm run build again. Verify that your code still works as before.

Exercise

  1. Install React and ReactDOM as the dependencies of the project.
  2. Bundle them together by including changes as described above.
  3. Verify that your code still works as before.

Commit: 040-webpack-dependencies

webpack-dev-server and html-webpack-plugin

webpack-dev-server is a webpack feature that would ease your development by starting up a web server to serve your code. We will use it with html-webpack-plugin so that even our index.html will be managed by webpack.

  1. install required packages as devDependencies:

    bash
    npm i -D webpack-dev-server html-webpack-plugin
    bash
    npm i -D webpack-dev-server html-webpack-plugin
  2. create a file webpack.config.js with the following content. webpack.config.js is the file that allows you to customize behavior of webpack. There are many configurations that you can do here, e.g. specify the entry points (if you entry point is not src/index.js) or the output folder (if you want to put the output in the folder with name other than dist).

    webpack.config.js
    js
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    /**
    * @type {import('webpack').Configuration}
    */
    module.exports = {
    devServer: {
    port: 9200,
    },
    plugins: [
    new HtmlWebpackPlugin({
    template: 'index.html',
    }),
    ],
    };
    webpack.config.js
    js
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    /**
    * @type {import('webpack').Configuration}
    */
    module.exports = {
    devServer: {
    port: 9200,
    },
    plugins: [
    new HtmlWebpackPlugin({
    template: 'index.html',
    }),
    ],
    };
  3. remove <script src="dist/main.js"></script> from index.html because webpack will help you to inject it.

  4. add a start npm script:

    package.json
    json
    {
    "scripts": {
    ...
    "start": "webpack-dev-server --mode=\"development\" --open"
    }
    }
    package.json
    json
    {
    "scripts": {
    ...
    "start": "webpack-dev-server --mode=\"development\" --open"
    }
    }
  5. run npm start (npm run start works too). This should open your default browser with the url http://localhost:9200/.

  6. try modify your code now. webpack will help you to refresh browser.

Bundling for production

You may realize that the current output file of webpack has tons of comments and spaces, which is good for us to read and understand what it is doing, but bad to be included to production as those comments are not useful for our customers/users.

To create a production bundle for our app:

  1. Add the following npm script in package.json:
    package.json
    json
    "build:prod": "webpack --mode=\"production\""
    package.json
    json
    "build:prod": "webpack --mode=\"production\""
  2. run npm run build:prod

Exercise

  1. configure webpack-dev-server as described and run it. Verify it recompile and refresh your browser when you make change to your code.
  2. configure build:prod npm scripts as described above. Verify the produced main.js is minified, and the file size is much smaller now.

Commit: 050-webpack-dev-server

Other features of webpack

Note that webpack is much more powerful than I’ve described above. The following common use cases of webpack are omitted here, but you should have a read after this workshop.

  • style-loader and css-loader: allow you to import .css file in your javascript file, which will be injected as <script> tag in html.
  • Hot Module Replacement used in conjunction with react-hot-loader allows you to tweak React components while maintaining state.
  • dotenv-webpack allows you to define environment variables. This is very commonly used to set environment specific variables, e.g. API endpoints, variables to set logging behavior etc.
  • webpack-merge allows you to merge webpack configurations. This is useful when you have multiple webpack configurations for different purpose (development optimized for compilation time, production optimized for runtime performance), and this package allows you to merge webpack configurations instead of copy-pasting the configurations.

Babel

Babel logo

  • In our current code, we use new JS feature/syntax like arrow-function, const and class. However, only modern browser recognize those JS syntax - it will cause parsing error in older browser like IE 11. You can confirm this by opening http://localhost:9200/ using IE.
  • Babel is the tool that allow us to write modern JS while maximizing backward compatibility.
  • Babel is a compiler that compile your modern JS code (usually called ES2015+, or ES6+) to the older JS code.
  • You can see Babel in action at https://babeljs.io/repl.

Compiling code with Babel

  • Babel allows few usage options, e.g. as a CLI tool or plugin to other build system.

  • Since we are using webpack, we will use babel-loader to integrate babel into our project.

  • To include Babel as part of our webpack bundling process:

    1. Install required packages:
      bash
      npm install -D @babel/core @babel/preset-env babel-loader
      bash
      npm install -D @babel/core @babel/preset-env babel-loader
    2. Create a .babelrc file with the following content. This file tells Babel what transformation to perform.
      .babelrc
      json
      {
      "presets": ["@babel/preset-env"]
      }
      .babelrc
      json
      {
      "presets": ["@babel/preset-env"]
      }
    3. Create a .browserlistrc file with the following content. This specifies what browser you supports. @babel/preset-env use this information to decide what syntax it needs to transform and what can be leave it as-is.
      .browserlistrc
      text
      > 1%
      not ie <= 8
      .browserlistrc
      text
      > 1%
      not ie <= 8
    4. Update webpack.config.js file by adding module.rules section, like the following content. This configuration tells webpack to run through all file ends with .js or .jsx extension through the babel-loader.
      webpack.config.js
      javascript
      module.exports = {
      module: {
      rules: [
      {
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      use: ['babel-loader'],
      },
      ],
      },
      devServer: {
      port: 9200,
      },
      plugins: [
      new HtmlWebpackPlugin({
      template: 'index.html',
      }),
      ],
      };
      webpack.config.js
      javascript
      module.exports = {
      module: {
      rules: [
      {
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      use: ['babel-loader'],
      },
      ],
      },
      devServer: {
      port: 9200,
      },
      plugins: [
      new HtmlWebpackPlugin({
      template: 'index.html',
      }),
      ],
      };
    5. Terminate your webpack-dev-server if it’s running by Ctrl+C. Run npm start start it again. This is because configuration change will not be picked up by webpack-dev-server while it is running.
    6. Open http://localhost:9200/ with IE and verify your code should works now.

Exercise

  1. Configure Babel as described above.
  2. Restart webpack-dev-server and verify that the application works in IE.

Commit: 060-babel-integration

Cleaning Up

As our code has been reorganized, with introduction of webpack, let’s update our npm scripts accordingly. Update format and lint script in package.json:

package.json
json
{
"scripts": {
...
"format": "prettier --write src/**/*.{js,jsx}",
"lint": "eslint src/**/*.{js,jsx} --quiet"
}
}
package.json
json
{
"scripts": {
...
"format": "prettier --write src/**/*.{js,jsx}",
"lint": "eslint src/**/*.{js,jsx} --quiet"
}
}