Advanced React + Webpack 4 + Babel 7 Web Application Setup

Advanced React + Webpack 4 + Babel 7 Web Application Setup

The world of building user interfaces can be a complex landscape to navigate. The sheer number of tools that are at the disposal of a developer is overwhelming. In my last tutorial , we discussed a few of those tools (React, Webpack and Babel) and went over the basics of what they are and how they work. More over, we also learnt how we can stitch them together to build an application code base from scratch that is suitable for development.

The application that was pieced together has minimal features. It does not allow us to test the code we're writing, among other things, and it's certainly not suitable for deploying to production. In this guide, we will build on top of the setup we have and take it further

  • Learn Dev + Production environment configs
  • Add test frameworks
  • Sass
  • ESLint
  • Static assets (images, SVG icons, font icons, font families)

The introduction segments can be skipped. Click here to jump straight to the step by step guide .

Environment configuration

An application consists of features and every feature has a life cycle --- from it being developed, then going through testing and finally being deployed to production, it lives on different environments (envs). The environments serve different purposes and therefore, their needs vary accordingly.

For instance, we don't care about performance or optimization in dev env, neither do we care about minifying the code. Often, we enable tools in dev env that helps us write code and debug it, like source maps, linters, etc. On the other hand, on prod env, we absolutely care about stuff like application performance and security, caching, etc. The tools we are going to use while walking through this guide is not going to play with all the items we discussed here, however, we will go through the basics (and some more) of how environment configuration works and why it is useful.

 

Test Frameworks

A test framework provides us with a platform and a set of rules that allows us to test the code we're writing. Any application that is intended to be deployed for users must be tested. Here is why:

  • It helps reduce the number of bugs --- and if we write new tests for the ones that do come up, we greatly minimize the chance of that particular bug re-appearing.
  • It gives us confidence when attempting to refactor code. A failing test would mean the refactored code did not satisfy that particular scenario.
  • Improves code quality, because developers are bound to write code that is testable, although writing good tests is an entirely different (and extremely valuable) skill of its own
  • All the reasons above reduce the overall cost of development in the long run (fewer bugs, better code quality, etc.)
  • Well-written tests become a form of documentation in itself of the code for which the test is being written.

The frameworks come in various different flavors --- and they all have their pros and cons. For our purposes, we will use two of the more popular frameworks, Jest to test functional JS and Enzyme to test our React components.

 

Sass

As the application grows in size, it starts to present maintainability and scalability concerns for developers. CSS is one such area where the code can get real messy real fast. Sass is a tool that helps us in this regard:

  • Compiles to CSS, so the end result is familiar code.
  • It allows nesting selectors. This enables developers to write cleaner and fewer lines of code and opens the door for more maintainable stylesheets.
  • It allows for creating variables, mixins, further promoting maintainability.
  • Conditional CSS, exciting stuff !!
  • It is industry approved --- performant and formidable community support.

No reason to not use a tool that will surely improve our development workflow, right?

 

ESLint

Another point of concern as the code base begins to grow is ensuring high standards of code quality. This is especially more important when there are multiple teams or developers working on the same code base. ESLint saves the day here --- it enforces common coding standards, or style guides, for all devs to follow. There are many industry-approved style guides out there, for instance Google and AirBnB. For our purposes, we will use the AirBnB style guide.

 

Static Assets

This encompasses all the pretty stuff that will be used in the application --- custom fonts, font icons, SVGs, and images. They are placed in a public folder, although an argument can be made for a different setup.

 

Please note: The rest of the guide builds on top of the last piece I wrote. You can either follow that first before proceeding here, or do the following:

  1. Ensure that that you have node version 10.15.3 or above. Open up your terminal and type in node -v to check. If the version does not match the requirements, grab the latest from here .
  2. Once you're good with the above, grab the repo and follow the installation instructions in the README.
  3. After installing the dependencies using npm install, run npm start to compile the code and spin up the dev server. At this point, you should see a new browser tab open, rendering a hello world component. Make sure you're inside the repository directory that you just "git cloned" before trying out the command.

 

After having gone through the basics of the tools we're about to use and set up our base repo, we can finally move forward to the guide.

 

Step 1

Assuming repo has been successfully downloaded, open it up in a text editor of your choice. You should see a file called webpack.config.js. This is where webpack configs currently live in their entirety.

In order to separate production and development builds, we will create separate files to host their configs, and another file will contain settings that are common between them, in the interest of keeping our code DRY.

Since there will be at least 3 config files involved, they will need to merge with each other at compile time to render the application. To do this, we need to install a utility package called webpack-merge to our dev dependencies.

npm install webpack-merge --save-dev

Then rename webpack.config.js to webpack.common.js. As the name implies, this will contain the common configs. We will create two more files

  • webpack.production.js --- to contain production env settings
  • webpack.development.js --- to contain development env settings

While we're on the subject of configuring webpack builds, we will take the opportunity to install a couple of npm packages that will help with our tooling and optimize our builds.

First, we will install a package called CleanWebpackPlugin.

npm install clean-webpack-plugin --save-dev

Webpack puts the output bundles and files in the /dist folder because that is what we've configured it to do. Over time, this folder tends to become cluttered as we do a build every time (through hot reloading) we make a code change and save. Webpack struggles to keep track of all those files, so It is good practice to clean up the /dist folder before each build in order to ensure the proper output files are being used. CleanWebpackPlugin takes care of that.

We will install another package called path. It will allow us to programmatically set entry and output paths inside webpack.

npm install path --save

 

Now that we have the necessary packages in place to configure a clean, optimized webpack build, let's change webpack.common.js to contain the following code,

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebPackPlugin = require("html-webpack-plugin");

module.exports = {
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            },
            {
                test: /\.html$/,
                use: [
                    {
                        loader: "html-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebPackPlugin({
            template: "./src/index.html",
            filename: "./index.html",
        })
    ]
};

Add the following lines to webpack.development.js

const merge = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist',
        hot: true
    }
});

... and these lines to webpack.production.js

const merge = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
    mode: 'production'
});

There are a few changes here from its previous iteration that requires explanation:

  • webpack.common.js
    • Note that we've added an output property. It renames the bundle file and defines the path to where it can be found.
    • We no longer have the dev server definition in here.
    • We are making use of CleanWebpackPlugin to clean up dist folder
  • webpack.development.js
    • The dev server definition has been moved to this file, naturally
    • We have enabled source maps
  • webpack.production.js
    • It only contains mode definition at the moment, but opens up the door to add additional tinkering later on.

That was a lot of information! We have accomplished a significant step towards setting up the project. Although I have tried my best to explain the concepts and code changes, I would advise additional reading into each of these topics to get a complete grasp. Webpack is a beast --- it might be a stretch even for the smartest developer to completely understand everything in the first read-through.

Let's move on to the next step.

 

Step 2

We will add test frameworks to our codebase in this step! There are two frameworks we need to add, one to test functional JS and the other to test React components. They are called Jest and Enzyme, respectively. Once we configure that, we will write a small, uncomplicated JS module and React component to try them out.

We will set them up and work with them in separate steps. Let's get started!

We will install Jest first as a dev dependency since it is a test framework and it has no use in the production bundle. To install,

npm install jest --save-dev

Next, we will add a file called jest.config.js to the root directory of our codebase that will dictate how we want to configure our tests. This is the official documentation page for Jest that contains details of every piece of configuration --- it is worth giving a read.

We will not need all the pieces, thus I have condensed the necessary pieces to write our own custom config file. It contains detailed comments on what each piece is doing. This is what jest.config.js file will look like for the project we're configuring

// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html

module.exports = {
    // All imported modules in your tests should be mocked automatically
    // automock: false,

    // Stop running tests after the first failure
    // bail: false,

    // Respect "browser" field in package.json when resolving modules
    // browser: false,

    // The directory where Jest should store its cached dependency information
    // cacheDirectory: "C:\\Users\\VenD\\AppData\\Local\\Temp\\jest",

    // Automatically clear mock calls and instances between every test
    clearMocks: true,

    // Indicates whether the coverage information should be collected while executing the test
    // collectCoverage: false,

    // An array of glob patterns indicating a set of files for which coverage information should be collected
    collectCoverageFrom: ['src/tests/*.test.js'],

    // The directory where Jest should output its coverage files
    coverageDirectory: 'src/tests/coverage',

    // An array of regexp pattern strings used to skip coverage collection
    coveragePathIgnorePatterns: [
      "\\\\node_modules\\\\"
    ],

    // A list of reporter names that Jest uses when writing coverage reports
    coverageReporters: [
      "json",
      "text",
      "lcov",
      "clover"
    ],

    // An object that configures minimum threshold enforcement for coverage results
    coverageThreshold: {
        "global": {
            "branches": 80,
            "functions": 80,
            "lines": 80
        }
    },

    // Make calling deprecated APIs throw helpful error messages
    errorOnDeprecated: false,

    // Force coverage collection from ignored files using an array of glob patterns
    // forceCoverageMatch: [],

    // A path to a module which exports an async function that is triggered once before all test suites
    // globalSetup: null,

    // A path to a module which exports an async function that is triggered once after all test suites
    // globalTeardown: null,

    // A set of global variables that need to be available in all test environments
    // globals: {},

    // An array of directory names to be searched recursively up from the requiring module's location
    // moduleDirectories: [
    //   "node_modules"
    // ],

    // An array of file extensions your modules use
    moduleFileExtensions: ['js', 'json', 'jsx'],

    // A map from regular expressions to module names that allow to stub out resources with a single module
    // moduleNameMapper: {},

    // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
    // modulePathIgnorePatterns: [],

    // Activates notifications for test results
    // notify: false,

    // An enum that specifies notification mode. Requires { notify: true }
    // notifyMode: "always",

    // A preset that is used as a base for Jest's configuration
    // preset: null,

    // Run tests from one or more projects
    // projects: null,

    // Use this configuration option to add custom reporters to Jest
    // reporters: undefined,

    // Automatically reset mock state between every test
    resetMocks: false,

    // Reset the module registry before running each individual test
    // resetModules: false,

    // A path to a custom resolver
    // resolver: null,

    // Automatically restore mock state between every test
    restoreMocks: true,

    // The root directory that Jest should scan for tests and modules within
    // rootDir: null,

    // A list of paths to directories that Jest should use to search for files in
    // roots: [
    //   "<rootDir>"
    // ],

    // Allows you to use a custom runner instead of Jest's default test runner
    // runner: "jest-runner",

    // The paths to modules that run some code to configure or set up the testing environment before each test
    // setupFiles: ['<rootDir>/enzyme.config.js'],

    // The path to a module that runs some code to configure or set up the testing framework before each test
    // setupTestFrameworkScriptFile: '',

    // A list of paths to snapshot serializer modules Jest should use for snapshot testing
    // snapshotSerializers: [],

    // The test environment that will be used for testing
    testEnvironment: 'jsdom',

    // Options that will be passed to the testEnvironment
    // testEnvironmentOptions: {},

    // Adds a location field to test results
    // testLocationInResults: false,

    // The glob patterns Jest uses to detect test files
    testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],

    // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
    testPathIgnorePatterns: ['\\\\node_modules\\\\'],

    // The regexp pattern Jest uses to detect test files
    // testRegex: "",

    // This option allows the use of a custom results processor
    // testResultsProcessor: null,

    // This option allows use of a custom test runner
    // testRunner: "jasmine2",

    // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
    testURL: 'http://localhost:3030',

    // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
    // timers: "real",

    // A map from regular expressions to paths to transformers
    // transform: {},

    // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
    transformIgnorePatterns: ['<rootDir>/node_modules/'],

    // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
    // unmockedModulePathPatterns: undefined,

    // Indicates whether each individual test should be reported during the run
    verbose: false,

    // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
    // watchPathIgnorePatterns: [],

    // Whether to use watchman for file crawling
    watchman: true,
};

According to our configuration, our tests should live inside a directory called tests inside /src. Let's go ahead and create that --- and while we're on the subject of creating directories, let's create three in total that will allow us to set ourselves up for future steps of the guide

  • tests - directory that will contain our tests
  • core/js - we will place our functional JS files here, the likes of helper, utils, services, etc.
  • core/scss - this will contain browser resets, global variable declarations. We will add these in a future piece.

Alright, we are making progress !! Now that we have a sweet test setup, let's create a simple JS module called multiply.js inside core/js

const multiply = (a, b) => {
    return a* b;
};

export default multiply;

... and write tests for it, by creating a file called multiply.spec.js inside tests directory.

import multiply from '../core/js/multiply';

describe('The Multiply module test suite', () => {
    it('is a public function', () => {
        expect(multiply).toBeDefined();
    });

    it('should correctly multiply two numbers', () => {
        const expected = 6;
        const actual1 = multiply(2, 3);
        const actual2 = multiply(1, 6);

        expect(actual1).toEqual(expected);
        expect(actual2).toEqual(expected);
    });

    it('should not multiply incorrectly', () => {
        const notExpected = 10;
        const actual = multiply(3, 5);

        expect(notExpected).not.toEqual(actual);
    });
});

The final piece of configuration is to add a script in our package.json that will run all our tests. It will live inside the scripts property

"scripts": {
    "test": "jest",
    "build": "webpack --config webpack.production.js",
    "start": "webpack-dev-server --open --config webpack.development.js"
  },

Now, if we run npm run test in our terminal (inside the root directory of the project), it will run all our tests and produce an output like this.

Screen Shot 2020-04-08 at 3.15.12 PM.png

You can keep adding more modules and test suites in a similar manner.

Let's move on to the next step!

 

Step 3

It's time to install Enzyme and test our React components! We need to install a version of Enzyme that corresponds to the version of React we're using, which is 16. In order to do that, we need to do the following, keeping in mind that this tool will also be installed as a dev dependency because, like Jest, the test framework does not need to be compiled to production bundle

npm install enzyme enzyme-adapter-react-16 --save dev

Next, we will create enzyme.config.js at the root directory of the project, similar to what we did for Jest. This is what that file should look like

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

Now, if you go take a look at line 119 in jest.config.js, you will see that we have done ourselves a favor by preparing for this moment where we set up Enzyme to work with Jest. All that needs to be done is uncomment line 119 and our setup will be complete!

Let's write a test for the <App /> component to see if what we've set up is working. Create a directory called components inside tests --- this will hold all the tests for the components you will write in the future. A separate directory is created to keep functional and component tests separate. This segregation can be done in any way, as long as all the tests live inside the src/tests directory. It will help in the future when the app starts to grow.

Inside src/tests/components directory, create a file called App.spec.js and add the following lines

import React from 'react';
import { shallow} from 'enzyme';

import App from '../../components/App';

describe('The App component test suite', () => {
    it('should render component', () => {
        expect(shallow(<App />).contains(<div>Hello World</div>)).toBe(true);
    });
});

Now if we run our test script in the terminal, you will see this test is running and passing!

Screen Shot 2020-04-09 at 3.13.21 PM.png

Please note: In step 2 and 3, we have simply set up Jest and Enzyme to work together in our code base. To demonstrate that the setup is working, we have written two overly simple tests. The art of writing good tests is an entirely different ball game and these tests should not be taken as any form of guide/direction.

 

Step 4

In this part of the guide, we will configure our codebase to lend .scss support. However, before we can learn to run, we need to learn to walk --- that means we will have to get css to load first.

Let's go grab the necessary npm packages

npm install css-loader style-loader --save-dev
npm install node-sass sass-loader --save

In the explanation block below, you can click the names of the tools that appear like this to visit their official documentation.

  • css-loader is a webpack plugin that interprets and resolves syntax like @import or url() that are used to include .scss files in components.

  • style-loader is a webpack plugin that injects the compiled css file in the DOM.

  • node-sass is a Node.js library that binds to a popular stylesheet pre-processor called LibSass. It lets us natively compile .scss files to css in a node environment.

  • sass-loader is a webpack plugin that will allow us to use Sass in our project.

Now that we have installed the necessary npm packages, we need to tell webpack to make use of them. Inside webpack.common.js, add the following lines in the rules array just below where we're using babel-loader and html-loader

{
    test: /\.s[ac]ss$/i,
    use: [
        // Creates `style` nodes from JS strings
        'style-loader',
        // Translates CSS into CommonJS
        'css-loader',
        // Compiles Sass to CSS
        'sass-loader',
    ]
}

The setup is complete! Let's write some sass !!

In src/components directory, create a file called App.scss and add the following lines

#app-container {
    letter-spacing: 1px;
    padding-top: 40px;

    & > div {
        display: flex;
        font-size: 25px;
        font-weight: bold;
        justify-content: center;
        margin: 0 auto;
    }
}

The explanation of sass syntax is beyond the scope of this article. This is an excellent resource for beginners to learn more in-depth.

Now, save the file and boot up the project by running npm run start. The application should load with the style rules we just wrote.

 

Step 5

It's time to install ESLint. Similar to what we've been doing so far, we need to install a few npm packages and then add a config file to our codebase. This is a tool that is needed purely for development purposes, so we will install it as a dev dependency.

Let's get started!

npm install eslint eslint-config-airbnb-base eslint-plugin-jest --save-dev
  • eslint-config-airbnb-base is the Airbnb style guide we're asking eslint to apply to our project.
  • eslint-plugin-jest is the eslint plugin for the jest test framework.

The Airbnb style guide has peer dependencies that need to be installed as well. You can input

npm info "eslint-config-airbnb@latest" peerDependencies

in your terminal and list them, however, to install, do the following

npx install-peerdeps --dev eslint-config-airbnb

Next, we need to create a file called .eslintrc.json (note the . at the beginning, indicating it's a hidden file)at the root directory of the project, similar to how the other config files (webpack, jest, enzyme, babel) have been added,

... and add these lines

{
    "extends": "airbnb",
    "plugins": ["jest"],
    "env": {
      "browser": true,
      "jest": true
    },
    "rules": {
      "arrow-body-style": [2, "always"],
      "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
      "no-unused-expressions": "off",
      "max-len": "off",
      "import/no-extraneous-dependencies": "off",
      "react/destructuring-assignment": "off",
      "react/prop-types": "off"
    }
}

The official documentation is a good read if you're looking to understand in detail how configuring ESLint works. The most pertinent line of code in that file is the rules object --- here we're basically overriding some of the rules from the style guide to suit the specific needs of our project. These are not set in stone, so please feel free to play with them to best suit your needs, but it's probably not a good idea to override too many of the rules --- that defeats the purpose of using a style guide in the first place.

Let's add a script to package.json that will apply the airbnb style guide to our codebase. We need to tell Eslint what files and/or directories we would like it to scan --- so we will tell it to scan all JS files

"lint": "eslint '**/*.js' --ignore-pattern node_modules"

Now, if you run npm run lint in your terminal, eslint will scan the file types and patterns specified in the script and display a list of issues. Fair warning, the project will have quite a few errors, but if you're using popular code editors like IDEA products, Visual Studio Code, Sublime, etc, they provide out of the box support to fix most of these issues in one quick stroke (format document).

If a large number of errors is proving to be a hindrance to your learning, please feel free to uninstall ESLint by running npm uninstall eslint eslint-config-airbnb-base eslint-plugin-jest --save-dev

 

Step 6

We're almost done with setting up our project --- the finish line is within our sights !! In this last step, we will configure our project to make use of various static assets like images, SVGs, icons, and custom typefaces.

Custom Typefaces

Any respectable front-end setup should have varying fonts displaying information on the page. The weight of the font, along with its size, is an indicator of the context of the text being displayed --- for instance, page or section headers tend to be larger and bolder, while helper texts are often smaller, lighter and may even be in italics.

There are multiple ways of pulling custom fonts into an application. Large enterprise codebases usually buy licenses to fonts and have their static assets as part of the server that hosts the application. The process to do that is slightly complicated --- we need a dedicated piece to walk through that.

The most convenient way of using custom fonts is to use a public domain library that has a large collection and is hosted on a CDN (Content Delivery Network), like Google Fonts. It is convenient because all we need to do is, select a couple of fonts we like and then simply embed their url in our static markup index.html

...and we're good to go !! So let's get started. For our purposes, we shall use the Roboto Mono font family.

Open up index.html and paste the following stylesheet link in the head

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">

We're done. Now we can use font-family: 'Roboto Mono' in any of our .scss files. We can use any number of fonts in this way.

Images

Images, like fonts, are an essential part of a front-end setup. In order to enable our project to utilize images in the application, we need to install a loader for webpack. This step is identical to what we've done multiple times in this guide --- install the loader and add a few lines to webpack config to make use of it

npm install url-loader --save-dev

... then add the following lines to the rules array in webpack.common.js

...
{
  test: /\.(jpg|png)$/,
  use: {
    loader: 'url-loader',
  },
},
...

The project is now ready to use images of type .jpg and .png . To demonstrate, create a public/images folder at the root directory of the project. Then add any image to the subdirectory images. For our purposes, I downloaded a free image from Unsplash and named it coffee.png

Next, we will create a directory inside src/components called Image --- then create the Image component.

Image.js

import React from 'react';

const Image = (props) => {
  return (
    <img
      src={props.src}
      alt={props.alt}
      height={props.height}
      wdth={props.wdth}
    />
  );
};

export default Image;

Then, import both the Image component and the actual image coffee.png in App.js. At this point, we will have to make minor edits to the App.js to use the image

import React from 'react';
import './App.scss';

// component imports
import Image from './Image/Image';

// other imports
import coffee from '../../public/images/coffee.png';

const App = () => {
  return (
    <div>
      <span>Hello World</span>
      <Image
        src={coffee}
        alt="hero"
        height="400"
        width="400"
      />
    </div>
  );
};

export default App;

Now, if you start the application, you will see the image is being loaded on the page.

Conclusion

That concludes our step-by-step guide to setting up a modern React project from scratch. There was a lot of information to digest here, but to think of it, we have also come a long way from the minimal setup we did earlier. I hope the guide has been helpful in learning some key concepts in the area of modern front-end setup tooling.

The future pieces I have planned for this series are

  • Learn the basics of containerization and how to deploy this project in a container.
  • Add bonus features to our projects, like JS docs, comprehensive test runner outputs (with colors and coverage percentages !), more package.json scripts, and global scss stylesheets like resets and variables.

Please feel free to leave a comment and share it with your friends. I will see you in the next piece!

The repo for the advanced setup can be found here .

 

References

  1. Webpack Environment Variables and Configuration
  2. Webpack Output Management
  3. Sass-loader
  4. The Absolute Beginners Guide to Sass
  5. ESLint configuration
  6. Google Web Fonts - Get Started