React - Webpack5/Typescript/Testing setup from scratch to Production - (Updated 2021)

ยท

25 min read

In this, I will layout the steps needed to create your react app from scratch using webpack. I won't explain everything in detail, however, I'll try to give you an understanding of concepts separately and how you may use them. These steps assume that you have Node setup and at least some bare-bones knowledge of React.

We'll Focus on

React.Js
WebPack
WebPack & React
Dev Server
Webpack Loaders
Hot Reloading
Building For Production
Typescript Integration
Testing Environment
Deployment

ReactJs

Let's start with React and see how it works as a library. Follow the steps below.

  • Create a folder with your project_name && cd into your project directory
  • Run yarn init -y to add a package.json file
  • Run yarn add react react-dom

    Note: package-lock.json will be created these enable new developers to get the same version as everyone else.

  • Create a folder named dist at the root and add an index.html file then add a script tag pointing to this React file in the node_modules just before the closing </body> tag. Should look like this

    Note: <script src="../node_modules/react/umd/react.development.js"></script>

  • Just below it add another script tag to create a element. Takes three args: an HTML tag name, props and children.

    <script> console.log(React.createElement('h1', null, 'Hello from react!')) </script>

  • Reload the browser and check the logs, a Js object will be printed in the console.log but can't be rendered to screen yet. That's what ReactDOM is going to do for us.

  • ReactDOM has a render method which takes two arguments: A React Element (the React.createElement()) and an HTML element container to wrap our React Element.
  • Just like React, we'll also add a script tag for ReactDOM. It's somehow similar

    <script src="../node_modules/react-dom/umd/react-dom.development.js"></script>

  • Add a <div /> tag to the body and give it an id="root". This div's id value will be ReactDOM render's second argument.

  • So let's mashup React and ReactDOM to render something on the screen.

    <script>
      ReactDOM.render(React.createElement('h1', null, 'Hello from react!'), document.getElementById("root"))
    </script>
    
  • Reload the screen and voila!!

Webpack

Webpack bundles all our js into static files which can easily be run by the browser while also minifing the code for optimization.

  • Create a new folder named src at the root of the project. We intend to bundle all our js in this src folder and inject it into the index.html
  • Install webpack as a dev-dependency yarn add webpack -D
  • Let's put webpack to a test, add an index.js file to the src folder. Add the following log console.log("Hello Webpack"). In your terminal enter the following command ./node_modules/.bin/webpack --mode=development ./src/index.js. This command is divided into three

    • ./node_modules/.bin/webpack - Running webpack
    • --mode=development - specify development mode
    • ./src/index.js - specify what we are bundling

      After running this command we will notice that dist/main.js is created at the root directory. dist/main.js contains our bundled code, to run it simply require it with the <script /> tag inside our dist/index.html. Refresh and check your console "hello Webpack". An example to bundle multiple files, create another file in src/ named maybe index2.js add any console.log statement, then require it inside index.js (add require(index2.js) at the top of the file) then rerun the command ./node_modules/.bin/webpack --mode=development ./src/index.js, refresh and check the console.

Webpack & React

Now that we have seen React & Webpack work separately, let's mash them up.

  • Empty src/index.js and copy the React code from the script tag in dist/index.html to src/index.js. Also remove all script tags dist/index.html except one with path dist/main.js. You may also delete index2.js if you created it.
  • At the top of src/index.js require React and ReactDOM i.e.
    const React = require('react'); 
    const ReactDOM = require('react-dom');
    

Configurations We won't have to run that webpack command all the time so let's automate it.

  • First we need an entry and output file. Entry file is that which requires all the other files (also files that require others and so on) and the output file is where our bundled code goes. We already have these files; entry - src/index.js, output - dist/main.js
  • Create our configuration file a webpack.config.js at the root of the project and add.
    module.exports = {
       mode: 'development',
       entry: __dirname + '/src/index.js',
       output: {
        path: __dirname + '/dist',
        filename: 'main.js',
        publicPath: '/',
      }
    }
    

    we have extracted variables from our inline command (node_modules/.bin/webpack --mode=development src/index.js) and added them to our configuration.

  • To run our command all we need is webpack command node_modules/.bin/webpack

Dev Server

It may get tiring to reload the page manually whenever a change is made. DevServer makes it possible for our application to automatically reload when changes are made to our code.

  • First we'll install the dev server yarn add webpack-dev-server -D.
  • Add this to the root of our webpack.config.js configuration file before the closing brace

    devServer: {
      contentBase: './dist',
      historyApiFallback: true,
      inline: true
    }
    

    contentBase - specifies the folder to serve
    historyApiFallback - makes it possible for an SPA to have navigation with multiple pages
    inline - Automatically refresh the page on file changes

  • Add "start": "webpack serve" to package.json script.

  • Run yarn start - here on, as long as your devServer is running, any changes to the code will trigger a browser refresh.

Webpack Loaders

Babel loader - We will need to write ES6 as opposed to ES5 (what we've been writing). Unfortunately, not all browsers support ES6, to enable this support we'll need a transpiler to convert our written ES6 to ES5 which is understandable by the other browsers. This is where babel comes in. We are going to add babel to our webpack build process so that we can transpile our code before bundling.

  • Firstly, run yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react babel-plugin-transform-class-properties -D to install the babel and other plugins.

    • babel-loader- is the most important among the bunch, it gets the code, transpiles it using the provided presets(preset-react and preset-env in this case), then pass it for bundling.
    • babel-plugin-transform-class-properties- enables us to use arrow functions outside render and also limit the reference scope of this to the App not global. In other words, preventing us from using bind(this) everywhere
  • Add a module object with a rules array to the root of our webpack.config.js file. In the rules, array is where we add an object to specify the loader configurations.

    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-react', '@babel/preset-env'],
              plugins: ['transform-class-properties']
            }
          }
        }
      ]
    },
    

    test: Specifies the types of files to be transpiled
    exclude: What should be skipped
    loader: What our loader is called
    options: What presets and plugins will be used by the loader

  • Run yarn start, if there are no errors - let's proceed to add React code. In case of any errors, the webpack configuration structure has been changing over time so check out their documentation.

  • In our src/index.js change the React.createElement(...) to <h1>...</h1>. also convert the require statements to import statements.
  • From now on you are free to use React.

Hot Reloading

Imaging reloading our application while preserving the current state and not reloading its entirety. Hot reloading enables you to update specific parts of the UI without reloading everything on the web app. We'll be using react-hot-loader
Steps to add hot reloading

  • Install react-hot-loader yarn add react-hot-loader
  • Add react-hot-loader/babel to babel plugins in webpack.config.js to make sure that our hot reloaded files are compiled by babel

         module.exports = {
              ...
              module: {
                  rules: [
                      {
                          ...
                          options: {
                              ...
                              plugins: ['transform-class-properties', 'react-hot-loader/babel']
                           }
                      }
                  ]
              }
    
  • Make the root component hot exported - this will send a new version of our app to ReactDOM.render when our files change

        import React from 'react'
        import { hot } from 'react-hot-loader/root';
    
        const App = () => {
            return (
                <div>
                    <h1>Hello Webpack</h1>
                </div>
            )
         }
    
         export default hot(App)
    
  • Finally to turn on hot reloading, add the --hot flag to your start script in package.json

         // package.json
         "scripts": {
             ...
             "start": "node_modules/.bin/webpack-dev-server --open --hot"
          },
    

    Or add hot: true to devServer in our webpack.config.js

         // webpack.config.js
         devServer: {
             ...
             hot: true
          },
    
  • Hot reloading should be working now. Try editing the text, you will notice that it will update without reloading the page.

But that's not all, because it does not support React Hooks. Let's add that support. We'll use @hot-loader/react-dom to replace react-dom because it has support for hot loading. Here is how.

  • Install the package yarn add @hot-loader/react-dom -D
  • Add it to webpack alias in webpack.config.js

      module.exports = {
          ...
          resolve: {
              alias: {
                  'react-dom': '@hot-loader/react-dom',
              },
          }
      }
    

    After this Hot reloading should be working fine with React Hooks.

    For more check the documentation

Building For Production

We've so far been working with development, now let's focus on deployment to production. When deploying we need to focus on the bare minimum of files required by the remote server to run our app.

  • An index.html file (Minified)
  • A CSS file (Minified)
  • A Javascript file (Minified)
  • All image assets
  • An asset manifest

Firstly, we'll use webpack to minify the index.html file and automatically add the appropriate style links and script tags, then add it to a build folder. To do this make a copy of our webpack.config.js file, name the new copy webpack.config.prod.js, as you might have already guessed this will contain our webpack configurations for production. We are going to make a few tweaks to this file since it contains things that may not be needed for production such as the dev-server and hot-reloading.

  • Remove all things concerning the devServer and hot-reloading i.e. the devServer key and its object value, also the react-hot-loader/babel value in the babel plugins array.
  • Since we are using the build folder, this time we'll change the pathname in output to __dirname + '/build' and also change the file to bundle.js (This one is optional)
  • Set the environment to production to avoid react warnings. require webpack and add this to the plugins array at the root of our webpack.config.prod.js.
    plugins: [
      new webpack.DefinePlugin({
        'process.env': {
          NODE_ENV: JSON.stringify('production')
        }
      }),
    ]
    
  • Run yarn add html-webpack-plugin -D to install html-webpack-plugin which will make the html minification for us.
  • Require html-webpack-plugin in our webpack.config.prod.js and add this below to our plugins array at the root of the object.
      new HtmlWebpackPlugin({
        inject: true,
        template: __dirname + "/dist/index.html",
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      }),
    
    inject - simply inserts a script tag inside our minified code. as we'll see
    template - Points to what we are minifying
    minify - minification specifications

Our webpack.config.prod.js should look something like this

const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    mode: 'production',
    entry: __dirname + '/src/index.js',
    output: {
        path: __dirname + '/build',
        filename: 'bundle.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-react', '@babel/preset-env'],
                        plugins: ['transform-class-properties']
                    }
                }
            }
        ]
    },
    resolve: {

    },
    plugins: [
        new webpack.DefinePlugin({
          'process.env': {
            NODE_ENV: JSON.stringify('production')
          }
        }),
        new HtmlWebpackPlugin({
            inject: true,
            template: __dirname + "/dist/index.html",
            minify: {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
              useShortDoctype: true,
              removeEmptyAttributes: true,
              removeStyleLinkTypeAttributes: true,
              keepClosingSlash: true,
              minifyJS: true,
              minifyCSS: true,
              minifyURLs: true,
            },
          })
      ]
}

Removed the alias object in resolve

  • Let's see how it works. first, add this line "build": "webpack --config webpack.config.prod.js" to scripts in the package.json. The --config fag specifies the configuration file to use. Run yarn build to see what happens.

    You'll notice a directory build/index.html created, When you open index.html. you'll see the minified code however the problem is that an extra script tag pointing to ./main.js is added. This is because of HtmlWebpackPlugin's inject key. To remove this problem, head back to your dist/index.html and delete the script tag point to main.js, since as we already noticed one is injected for us automatically. Also, copy our HtmlWebpackPlugin to webpack.config.js but without the minify key then rerun yarn build.

Adding css

If you haven't noticed already your app can't render CSS. Let's enable this.

  • Run yarn add css-loader style-loader -D check the documentation for more here
  • Add this object to our rules array in both webpack config files under babel-loader
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        },
    

    Here the loader grabs our CSS files and injects them into our javascript

And that's all you need for CSS, go on to test!!

Uglifying/Minifying to improve performance

Note: This section is irrelevant since webpack 4+ minifies your code by default in production. But if you want to look, here is a great webpack optimization tool terser(Installed automatically for webpack 4+) as opposed to what we used uglifyjs. Also uglifyjs below is deprecated feel free to skip it.

Now to our javascript in bundle.js, you'll notice that it spans a number of lines which is not great for performance. We are going to limit it to only one line to make it faster. To achieve this, we are going to run yarn add uglifyjs-webpack-plugin -D, require it and then add this to the root of our object

optimization: {
    minimizer: [
      new UglifyJsPlugin({
        sourceMap: true,
        uglifyOptions: {
          warnings: false,
          output: {
            comments: false,
          },
        },
      }),
    ],
  },
  • Run yarn build, you'll notice a single line of js in bundle.js

Handling Files (we ain't done ๐Ÿ˜…)

Onto adding a file-loader, we also want to make sure that our assets are copied from dist to build, to make sure they are accessible in production.

  • Let's run yarn add file-loader -D to install the file-loader
  • Add this to both our webpack files
       {
          test: /\.(png|jpe?g|gif)$/i,
          loader: 'file-loader',
       }
    
    File-loader only deals with files required by Javascript, but how about those required by our index.html like the favicon. To achieve this we'll not use webpack but javascript.
  • Create a folder scripts at the root of the project and a file copy_assets.js
  • Run yarn add fs-extra to install fs-extra, require it in our copy_assets.js
  • Add a script to copy all files in dist except index.html to build. copy_assets.js should look like this
const fs = require('fs-extra');

fs.copySync('dist', 'build', {
  dereference: true,
  filter: file => file !== 'dist/index.html'
})
  • Change build in package.json to node scripts/copy_assets.js && webpack --config webpack.config.prod.js and run yarn build

Caching

Add an Asset Manifest We want to list all static assets that we are generating, this will be useful when we start caching them to save load times.

  • Run yarn add webpack-manifest-plugin -D, require WebpackManifestPlugin a named import from webpack-manifest-plugin and then use it in our plugins for the webpack.config.prod.js file.
...
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");

module.exports = {
    ...
    plugins: [
      ...
      new ManifestPlugin({
        fileName: 'asset-manifest.json',
      })
}
  • Run yarn build an asset-manifest.json will be created in the build folder. opening build/index.html will produce the same outcome as when you run yarn start

Typescript Integration

For details on setup check the webpack docs
We begin by installing Typescript, ts-loader(Typescript loader) and some types as dev dependencies

  • Run yarn add -D typescript ts-loader @types/react @types/react-dom @types/jest @types/html-webpack-plugin
  • Create your tsconfig.json file at the root and add the configurations below
    {
     "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "es6",
        "target": "es5",
        "jsx": "react",
        "allowJs": true,
        "strict": true,
        "esModuleInterop": true,
        "moduleResolution": "node"
      }
    }
    
  • Update the webpack.config.js and webpack.config.prod.js.
    • Change entry point to __dirname + '/src/index.tsx'
    • Swap the babel configurations with the ts-loader
    • Switch /.js$/ with /.tsx$/
    • Add an array of extensions inside the resolve object
      module.exports = {
      ...
      entry: __dirname + '/src/index.tsx',
      devtool: 'inline-source-map',
      ...
      module: {
          rules: [
              {
                  test: /\.tsx$/,
                  exclude: /node_modules/,
                  use: 'ts-loader',
              },
              ...
          ]
      },
      resolve: {
          extensions: ['.tsx', '.ts', '.js'],
          ...
      },
      ...
      }
      
  • Currently, we can't use assets like images, to enable that we create a custom.d.ts file at the root of our project and add the following declarations.
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
  • Change all files inside src to tsx and ts respectively
  • Run yarn start

That's all we need to integrate Typescript!!

Testing Environment

We are going to integrate the react-testing-library(why it's better than enzyme). We'll start by installing jest, ts-jest, @testing-library/react and their corresponding types and dependencies.

  • Run yarn add -D ts-jest jest jsdom @types/jest @testing-library/react @testing-library/jest-dom
  • Run yarn ts-jest config:init to generate our jest.config.json with some default configurations.
  • Inside jest.config.json change the testEnvironment value to 'jsom'
  • Jest will naturally render our static data as ts files which will cause errors, so we'll have to use mocks, to tell jest to use the mocks whenever attempting to access static files. Create a mocks folders at the root of your project and inside it add a fileMock.ts file.
  • Inside the jest.config.json add amoduleNameMapperfield to specify all static file types.
// jest.config.json
testEnvironment: 'jsdom',
moduleNameMapper: {
    "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.ts",
    "\\.(css|less)$": "<rootDir>/__mocks__/fileMock.ts"
  }
// fileMock.ts
module.exports = '';

That's all we need to integrate the testing environment, now to run the tests we'll add a script inside the package.json.

{
  ...
  "scripts": {
    "test": "yarn jest --coverage",
    ...
  },
  ...
}

To test this, let's write a simple failing test and run it. Inside our src create a file named App.spec.tsx.

import React from 'react'
import { render } from '@testing-library/react'
import App from "./App"

describe('App.js', () => {
    test('should be true', () => {
        expect(true).toEqual(true)
    })

    test('should fail until we add 'hello world' in our document' , () => {
        const { getByText, container } = render(<App />)
        expect(getByText("hello world")).toBeInTheDocument()
    })
})

Run yarn test, tests should fail To make that pass add a hello world tag inside your App.tsx

Deployment

We going to deploy our app to vercel, but first, let's initialize our project as a github repo.

  • Run git init (hopefully you have git install and have a github account)
  • Create a .gitignore and add irrelevant files like the node_modules directory
  • Run git add . then git commit -m "initial commit"
  • Create a new repo and push the codebase.

Vercel

  • Create a vercel account (if you don't have one) and connect your github account.
  • Select the repo you wish to deploy.
  • In the Build and Output Setting override OUTPUT DIRECTORY and add build if it isn't already.
  • Deploy!!

THAT'S ALL

Refer to the complete code here: github.com/ezrogha/react-webpack-typescript

Congrats !!!

If you get stuck at any stage, have suggestions or questions, feel free to drop a comment.

References