๐Ÿ“Project Setup

This guide is to help understand what goes on under the hood of CRA application, and to grasp how the tools like Webpack and Babel work in tandem with React and Typescript.

Motivation

I had fun with create-react-app, with the no-worries setup it provides to dive right into the action of writing React components, but now it's time we get our hands dirty with the configuration that create-react-app does for us under the hood. This is basically to understand the basic concepts of how tools like Webpack and Babel help us developers in writing better code, with the latest ECMAScript standards, while also ensuring that older browsers that do not support these latest standards, also work for the code that we write.

P.S. This is a really long article, stick till then end, it's going to be worth the effort! All the code for this guide is available on Github.

1. Base setup

Let's start by creating a folder and initializing git and yarn, plus we'll also add two folders, src and public, which will house our code.

mkdir boilerplate; cd boilerplate
# initialize package.json
yarn init -y;
#  initialize git version control
git init

React dependencies

Let's add the react and react-dom dependencies:

yarn add react react-dom

Typescript & others

Let's add the Typescript and other dev dependencies: [use the -D flag to save as dev dependency]

yarn add -D typescript @types/react @types/react-dom

With these dependencies added, we need to add a new file to the root directory of the app, called tsconfig.json. This file will contain all the configuration required for Typescript in our app.

tsconfig.json
{
  "compilerOptions": {
    "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
    "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
    "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ /* Type declaration files to be included in compilation. */,
    "lib": [
      "DOM",
      "ESNext"
    ] /* Specify library files to be included in the compilation. */,
    "jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', 'react' or 'react-jsx'. */,
    "noEmit": true /* Do not emit outputs. */,
    "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */,
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
    "strict": true /* Enable all strict type-checking options. */,
    "skipLibCheck": true /* Skip type checking of declaration files. */,
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
    "resolveJsonModule": true
    // "allowJs": true /* Allow javascript files to be compiled. Useful when migrating JS to TS */,
    // "checkJs": true /* Report errors in .js files. Works in tandem with allowJs. */,
  },
  "include": ["src/**/*"]
}

Babel & others

With the Typescript configuration in place, let's add babel and it's dev dependencies:

yarn add -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript

Create a new file in the root directory called .babelrc, which will contain the config options for babel.

.babelrc
{
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      {
        "runtime": "automatic"
      }
    ],
    "@babel/preset-typescript"
  ],
}

Base Code

With this basic setup in place, we can now write Typescript JSX, i.e, TSX in our app! But keep in mind, we can only write it, currently it cannot be compiled and displayed on the browser, for which we will need Webpack!

src/index.tsx
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root');

2. Webpack

We need to inject our <script> into the index.html file. We do this by using webpack, which will transpile and bundle our React app into plain Javascript, and inject it into the HTML page in our app.

Webpack & others

Let's add webpack and it's dev dependencies! Also, I'll be using a really cool webpack dashboard to see the status of my compilation and bundling, it's called webpack-dashboard, and is open sourced by FormidableLabs.

yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-dashboard

With the above tools installed, we will need to create a new "script" in our package.json file, which points to the webpack configuration of our app. But before we start with the webpack configuration, we need to add another package, called babel-loader, which is required to transpile all the .tsx and .jsx files in out app!

yarn add -D babel-loader

Configuration

Let's configure webpack. For this, we need to create a new file called webpack.config.js at the root level directory of our app.

webpack.config.js
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const DashboardPlugin = require('webpack-dashboard/plugin')

module.exports = {
  entry: path.resolve(__dirname, './src/index.tsx'),
  mode: 'development',
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, './build'),
    filename: 'bundle.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: path.resolve(__dirname, './public/index.html')
    }),
    new DashboardPlugin()
  ],
  devServer: {
    port: 3000,
    open: true
  }
}

With the configuration done, we need to add the script to the package.json file:

"script" : {
  "start": "webpack-dashboard -- webpack serve --config webpack.config.js"
}

We can finally test our app on the browser! Simply run yarn start in the terminal to see the app working on localhost:3000.

3. Prod and Dev Webpack Configs

It's usually a good idea to split out webpack configuration into different files. We usually 4 webpack config files:

  • webpack.dev.js : This contains the config for the webpack dev server

  • webpack.prod.js: This contains the config for building our app for production

  • webpack.common.js: This contains the configs that are common to both dev & prod

  • webpack.config.js: This 'merges' both dev OR prod config file AND the common config file

Let us setup this structure for our app! We'll create a webpack/ folder in the root directory of our app, and create 3 files in it, viz., webpack.common.js, webpack.dev.js and webpack.prod.js. We would have to change the content of webpack.config.js (which is in the root directory, and not the webpack folder) as well. But first, we need to install a package that merges two webpack configuration files.

yarn add -D webpack-merge

With this package installed, we are ready to configure our webpack configuration files!

webpack/webpack.common.js
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
const DashboardPlugin = require('webpack-dashboard/plugin')

module.exports = {
  entry: path.resolve(__dirname, './src/index.tsx'),
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, './build'),
    filename: 'bundle.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      template: path.resolve(__dirname, './public/index.html')
    }),
    new DashboardPlugin()
  ]
}

4. Adding styles

Since there are many options out there to style a React app, we'll be focusing on really the basic tools for styling an app, i.e, CSS, CSS-modules and SASS. These are the most widely used styling tools used and are relatively easy to set up with webpack! We will add different rules for development and production modes.

Installing dev dependencies

Before we start adding rules to webpack config files for styling, we need to install a few loaders that can handle CSS and SASS:

yarn add -D style-loader css-loader sass-loader node-sass mini-css-extract-plugin

Webpack Configs for Styling

webpack/webpack.dev.js
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.(scss|css)$/,
        include: /\.module\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoader: 1,
              modules: {
                localIndentName: '[name]_[local]_[hash:base64]',
                auto: true
              },
            }
          },
          'sass-loader
        ]
      },
      {
        test: /\.(scss|css)$/,
        exclude: /\.module\.css$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      }
    ]
  },
  //...
}

Add typings.d.ts

With these configurations in place, we need to do one final thing, add a new file in the src/ folder called typings.d.ts , which will have the modules declaration in it. Let's add some basic modules that we will declare in this file, and then move on to add configurations for using images and other stuff in our app with webpack!

src/typings.d.ts
declare module '*.module.css'
declare module '*.scss'
declare module '*.css'
declare module '*.svg'

5. Images and SVGs

All apps require images and SVGs in their code. To bundle such files in our app with webpack, we do not need to install any extra dependencies! Prior to webpack 5 & 4, we had to install the 'file-loader' dependency with other things to use images and SVGs in our app. Now, we need to just use the type property set to asset/resource or asset/inline to use images or SVGs in our app respectively. Not only images and SVGs, we can also use other file with file-extenstions like .gif, .jpg, .jpeg, .woff, .eot, etc.

We will declate these configs in our common webpack config file:

webpack/webpack.common.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(?:ico|png|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
      {
        test: /\.(woff(2)?|eot|ttf|ots|svg|)$/,
        type: 'asset/inline',
      },
    ]
  }
}

6. Misc Webpack Dependencies

Here are a few common dependencies that are typically used in every production ready application. They are not absolutely necessary to install, but it's recommended that you do, since they are pretty useful!

React Refresh Webpack Plugin

This is a plugin that adds hot reloading to our app.

yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
webpack/webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
  //...
  devServer: {
    //...
    hot: true
  },
  plugins: [new ReactRefreshWebpackPlugin()]
}

Dotenv Webpack Plugin

This plugin helps us use environment variables throughout our app, and utilizes the functionality of webpack.DefinePlugin to declare global constants which can be declared at compile time, and therefore helps us hide important information for the source code and the end-user.

yarn add -D dotenv-webpack
webpack/webpack.common.js
//...
const Dotenv = require('dotenv-webpack')

module.exports = {
  //...
  plugins: [new Dontenv()]
}

With this setup in place, we can define a .env file in our app's root directory and declare global constants that we can define during runtime.

Update package.json

We need to add the scripts to the package.json file from where we can run the webpack server for production or development. We'll add the rimraf package as well, to delete all previous build folders, so as to start a clean build every time we build our app!

yarn add -D rimraf
./package.json
{
  //...
  "scripts": {
    "start": "webpack-dashboard -- webpack serve --config webpack.config.js --env env=dev",
    "prebuild": "rimraf build",
    "build": "webpack --config webpack.config.js --env env=prod"
  }
  //...
}

7. ESLint

The ESLint package helps use developers write code that is as accurate as possible by pointing our errors as we write our code. Pretty useful feature to have linting in our code to highlight errors and warnings that indicate problems with our code.

A prerequisite for this dependency is that we need to have the ESLint extension installed in VSCode. This extension can be found here.

yarn add -D eslint eslint-plugin-react eslint-plugin-react-hooks
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D eslint-pligin-import eslint-plugon-jsx-a11y

Now, we have to define the configurations for ESLint in a new file at the app's root directory, called '.eslintrc.js'.

.eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
  extends: [
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:import/typescript',
    'plugin:jsx-a11y/recommended',
  ],
  rules: {
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': ['error'],
    '@typescript-eslint/no-var-requires': 'off',
    'react/prop-types': 'off',
    'react/jsx-uses-react': 'off',
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
  },
}

Let's take it to the next level by adding a script to run eslint on our files:

package.json
{
  "scripts": {
    "lint": "eslint --fix \"./src/**/*.{js,jsx,ts,tsx,json}\""
  }
}

8. Prettier

This is basically a style guide for our code, and helps maintain a standard for writing code for an application. Again, a prerequisite for this to work is we have the Prettier extensions installed on VSCode. This extension can be found here.

yarn add -D prettier eslint-config-prettier eslint-plugin-prettier

To configure prettier, a '.prettierrc.js' file is required at the app's root directory.

.prettierrc.js
module.exports = {
  semi: false,
  trailingComma: "es5",
  singleQuote: true,
  jsxSingleQuote: false,
  printWidth: 80,
  tabWidth: 2,
  endOfLine: "auto",
}

We need to modify the .eslintrc.js file a little bit as well:

.eslintrc.js
module.exports = {
  //...
  extends: [
    //...
    'prettier',
    'prettier/@typescript-eslint',
    'plugin:prettier/recommended',
  ],
  //...
}

Time to take it to the next level, by adding a script to run prettier on all our files:

package.json
{
  "script": {
    "format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
  }
}

We can also turn on the 'Format on Save' option in VSCode to true, to format our files when we save them.

9. Husky & lint-staged

With Husky and lint-staged packages, we prevent linting and formatting errors to be committed to a common work repository for our app. This help maintain a code standard, and thus prevents accidental errors in our code or formatting of our code to be committed into the repository.

yarn add -D husky@4 lint-staged

Define lint-staged configs

package.json
{
  //...
  "lint-staged": {
    "src/**/*.{js,jsx,ts,tsx,json}": [
      "eslint --fix"
    ],
    "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
      "prettier --write"
    ]
  }
}

Define Husky configs

We define the husky config that tells husky to run the lint-staged configurations just before the code is being committed.

package.json
  {
    //...
    "husky": {
      "hooks": {
        "pre-commit": "lint-staged"
      }    
    }
  }

10. Nice-to-have(s)

Below are some other configurations for our React + Typescript app that are not necessarily required, but are good to have.

Babel & Runtime

These plugins let us us the async-await features in our application.

yarn add -D @babel/runtime @babel/plugin-transform-runtime

We'll update the .babelrc file to incorporate the plugins.

.babelrc
{
  //...
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "regenerator": true
      }
    ]
  ]
}

Copy Webpack Plugin

This plugin helps us copy static assets from a source to a destination, typically used to copy the contents of the source folder into the build folder, when the build command is run.

yarn add -D copy-webpack-plugin
webpack/webpack.common.js
//...
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  //...
  plugins: [
    new CopyWebpackPlugin({
      patterns: [{from : './src/**/*', to: './build' }]
    })
  ]
}

Bundle Analyzer Plugin

This plugin runs when we build our app for production. It loads a webpage that displays all the bundled files and their sizes. Pretty useful feature to use in an app that is being deployed.

yarn add -D webpack-bundle-analyzer
webpack/webpack.prod.js
//...
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  //...
  plugins: [new BundleAnalyzerPlugin()]
}

Last updated