Lewislbr

Set up a project with React, TypeScript, Babel, webpack, Prettier, and ESLint

This is a basic guide to get a React project started from scratch without Create React App or similar. It is the way to go if you need control over any tool or if you want to know how all the pieces work together. Additional configuration may be required depending on the tools, plugins and packages used in your project.

Create A package.json File

First we'll create a JSON file to store project information, scripts and dependencies of our project. Open the terminal on the project folder and type:

npm init --yes

Once it's created we can add some scripts to start and build our app with defined instructions. In this case we'll specify to run the TypeScript compiler to check for errors, clean the output folder to reduce the possibility of build conflicts, and start webpack to run the app for development or production. Also, we'll add scripts for linting and formatting our code.

{
  "scripts": {
    "start": "webpack serve --mode=development",
    "build": "rm -rf dist && webpack --mode=production",
    "lint": "eslint --ext .ts,.tsx src",
    "format": "prettier --write --check src"
  }
}

Set Up Babel

Then we'll install Babel, a JavaScript compiler that takes modern code and transforms it into code that all browsers understand, with some presets we'll need:

npm i -D @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript babel-loader

After that, we'll create a Babel configuration file:

touch .babelrc

And then add some configuration to work with:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {"esmodules": true}
      }
    ],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}

Set Up webpack

Now we'll install webpack, a module bundler for JavaScript that will optimally bundle our app:

npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin

And then we'll create a configuration file that will handle the development and production builds:

touch webpack.config.js

Where we'll add the settings we're going to use:

/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-var-requires */
const webpack = require("webpack")
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = (env, options) => {
  const isDevelopment = options.mode !== "production"

  return {
    mode: isDevelopment ? "development" : "production",
    target: "web",
    entry: "./src/index.tsx",
    output: {
      filename: isDevelopment ? "[name].js" : "[name].[contenthash:8].js",
      path: path.join(__dirname, "/dist"),
    },
    resolve: {
      extensions: [".ts", ".tsx", ".js"],
    },
    module: {
      rules: [
        {
          test: /\.ts(x)?$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
            },
          },
        },
      ],
    },
    devtool: isDevelopment ? "eval-cheap-module-source-map" : "nosources-source-map",
    optimization: {
      runtimeChunk: {
        name: "runtime",
      },
      splitChunks: {
        chunks: "all",
        cacheGroups: {
          defaultVendors: {
            name: "vendors",
            test: /[\\/]node_modules[\\/]/,
          },
        },
        name: false,
      },
    },
    performance: {
      hints: false,
    },
    plugins: [
      new HtmlWebpackPlugin({
        minify: isDevelopment
          ? false
          : {
              collapseWhitespace: true,
              keepClosingSlash: true,
              minifyCSS: true,
              minifyJS: true,
              minifyURLs: true,
              removeComments: true,
              removeEmptyAttributes: true,
              removeRedundantAttributes: true,
              removeScriptTypeAttributes: true,
              removeStyleLinkTypeAttributes: true,
              useShortDoctype: true,
            },
        template: "./src/index.html",
      }),
      ...(isDevelopment ? [new webpack.HotModuleReplacementPlugin()] : []),
    ],
    stats: {
      assetsSort: "!size",
      colors: true,
      entrypoints: false,
      errors: true,
      errorDetails: true,
      groupAssetsByChunk: false,
      groupAssetsByExtension: false,
      groupAssetsByInfo: false,
      groupAssetsByPath: false,
      modules: false,
      relatedAssets: true,
      timings: false,
      version: false,
    },
    devServer: {
      contentBase: "dist",
      historyApiFallback: true,
      host: "0.0.0.0",
      hot: true,
      port: 8080,
    },
  }
}

With this file we tell webpack which files to use as input, where to output the bundles, what loader to use to process them, separate them by type, define a different output filename and devtool for each mode, define a performance budget, and disable HTML minification and files compression for development. Additionally, we add the settings of the development server and how to reload the components that change without refreshing the page.

For more information please check the official documentation.

Set Up React

Now it's time to install React, a JavaScript library for building user interfaces:

npm i react react-dom

Then we'll create and go to a new folder to store React files:

mkdir src && cd src

And we'll create our main component of the app:

touch App.tsx

Where we will simply render a couple of HTML tags by now:

import React from "react"

export function App(): JSX.Element {
  return (
    <>
      <h1>Project Name</h1>
      <p>This is going to be great.</p>
    </>
  )
}

Then we'll create a new file:

touch index.tsx

Where we'll add some code to connect our app with the base HTML, and enable webpack's hot reloading when using the development server:

import React from "react"
import ReactDOM from "react-dom"
import {App} from "./App"

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

if (module.hot) {
  module.hot.accept()
}

Now we'll create the base HTML file where all the app will be built upon:

touch index.html

And write some basic structure:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Project Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <noscript>JavaScript is needed to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Finally, we can go to the root folder to continue setting up the project:

cd -

Set Up TypeScript

Next, we'll install TypeScript, a strict syntactical superset of JavaScript, which adds optional static typing. We can install it, along with some React-related packages, with:

npm i -D typescript @types/react @types/react-dom @types/webpack-env

Once installed we'll create a configuration file:

touch tsconfig.json

Where we'll add some basic rules to start with:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "jsx": "react",
    "lib": ["DOM", "ESNext"],
    "noEmit": true,
    "skipLibCheck": true,
    "strict": true
  }
}

This is highly configurable and there are many other rules available if needed, you can find more info here.

Set Up Prettier

Then we'll install Prettier, a configurable code formatter:

npm i -D prettier

And also create a configuration file:

touch .prettierrc

Where we'll add some options, most of them with the default value, for consistency, specially if working in a team. There are some other options that can be checked here.

{
  "arrowParens": "always",
  "bracketSpacing": false,
  "endOfLine": "auto",
  "htmlWhitespaceSensitivity": "css",
  "insertPragma": false,
  "jsxBracketSameLine": false,
  "jsxSingleQuote": false,
  "printWidth": 80,
  "proseWrap": "preserve",
  "quoteProps": "as-needed",
  "requirePragma": false,
  "semi": false,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "none",
  "useTabs": false
}

Prettier configuration is pretty subjective. A team should decide what the standard should be, and explicitly declare as many rules as possible to avoid conflicts with individual IDE settings.

Set Up ESLint

Finally, we'll install ESLint, a linting tool that will help us write better code and show us errors we've made. We'll install it with packages for the other tools we use:

npm i -D eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser

Then we'll create a configuration file:

touch .eslintrc

And add some basic rules:

{
  "env": {
    "browser": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended"
  ],
  "ignorePatterns": ["dist"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint", "react", "react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

ESLint rules are very configurable, there are many others that you can find here, and you can turn off any of them if needed. They should also be a team standard.

Start The Project

Now the project is ready to work on it! To get the project started, we can run:

npm run start

From here, depending on your needs you may start adding other pieces like a router or a server.



If you're using dark mode, do you like the code blocks's theme? I have it available for VS Code, feel free to check it.