React + Webpack + Babel 7 + TypeScript Starter

I have been recently working on projects that uses TypeScript with React. The easiest way to get started working with React + TypeScript is through the create-react-app boilerplate. (react-scripts@2.1.0 and above now supports typescript) You can now easily add TypeScript support by adding the --typescript flag.

I got curious on how we can setup a TypeScript React app without the help of create-react-app. So today I’ll be sharing what I have learned this past few days.

In this post, I will be creating TypeScript React app from scratch without using the create-react-app boilerplate. This will also cover additional stuff such as integrating CSS Modules and adding ESLint with typescript-eslint parser.

I initially wrote this article as a cheatsheet for myself. So if you find something wrong or find something that can be improved, please do leave a message. 😅I’ll be updating this post from time to time.

Getting Started

This is going to be the folder structure:

react-webpack-babel-typescript/
├── config/
│   ├── index.html
│   ├── webpack.common.js
│   ├── webpack.dev.js
│   └── webpack.prod.js
├── src/
│   ├── components/
│   │   └── Title.tsx
│   ├── types/
│   │   └── css-modules.d.ts
│   ├── App.css
│   ├── App.tsx
│   ├── global.css
│   └── index.tsx
├── .babelrc
├── .eslintrc.js
├── .gitignore
├── package.json
└── tsconfig.json

Let’s start off by creating a new project directory and generate package.json file with the command below:

mkdir typescript-react && cd typescript-react && npm init -y

The command above will create a directory called typescript-react and generates a package.json file inside the project directory.

Webpack

Install webpack and its plugins:

npm i -D webpack webpack-cli webpack-dev-server webpack-merge html-webpack-plugin clean-webpack-plugin
  • webpack-dev-server – provides you with a simple web server and the ability to use live reloading.
  • webpack-merge – a utility to merge webpack configurations.
  • html-webpack-plugin – a plugin that simplifies creation of HTML files to serve your bundles
  • clean-webpack-plugin – a plugin to remove/clean your build folder
config/webpack.common.js

I will start by writing the common webpack configuration that will be used for both development and production.

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

const config = {
  entry: path.join(__dirname, '../src/index'),
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, './index.html'),
      minify: {
        removeComments: true,
        collapseWhitespace: true,
      },
    }),
  ],
};

module.exports = config;
config/webpack.dev.js

This is the development specific configuration

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

const config = {
  mode: 'development',
  devtool: 'inline-source-map',
  output: {
    path: path.join(__dirname, '../build'),
    filename: 'bundle.js',
  },
};

module.exports = merge(common, config);
config/webpack.prod.js

This is the production specific configuration

const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
const common = require('./webpack.common');

const config = {
  mode: 'production',
  devtool: 'source-map',
  output: {
    path: path.join(__dirname, '../build'),
    filename: 'js/main.[contentHash].js',
    publicPath: './',
  },
  plugins: [new CleanWebpackPlugin()],
};

module.exports = merge(common, config);
config/index.html

This is the HTML template thats going to be used by the HTMLWebpackPlugin

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>React + Webpack + Babel 7 + TypeScript</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
package.json

We will add the following scripts to package.json file

{

  // ...

  "scripts": {
    "start": "webpack-dev-server --mode development --open --hot --config config/webpack.dev.js",
    "build": "webpack --mode production --config config/webpack.prod.js",
  },
  
  // ...

}

Babel

npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
.babelrc
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}
config/webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const config = {
  
  // ...entry, resolve

  module: {
    rules: [
      {
        test: /\.(ts)x?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
  
  // ...plugins
};

module.exports = config;

TypeScript

npm i typescript
tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "strictNullChecks": true,
    "noEmit": true,
    "jsx": "react",
    "lib": ["es6", "dom"],
    "module": "commonjs",
    "allowSyntheticDefaultImports": true
  },
  "include": ["./src"],
  "exclude": ["node_modules", "build"]
}

React

npm i react react-dom @types/react @types/react-dom

It’s time to write some React components.

src/index.tsx

This is going to be our main entry point

import React from 'react';
import { render } from 'react-dom';
import App from './App';

render(<App />, document.getElementById('root'));
src/App.tsx
import React, { FC } from 'react';
import Title from './components/Title';

const App: FC = () => (
  <div>
    <Title title="React + Webpack + Babel 7 + Typescript" />
  </div>
);

export default App;
src/components/Title.tsx
import React, { FunctionComponent } from 'react';

interface Props {
  title: string;
}

const Title: FunctionComponent<Props> = ({ title }) => (
  <h1>{title}</h1>
);

export default Title;

We can now start our app

npm start

ESLint

npm i -D eslint eslint-plugin-react @typescript-eslint/parser @typescript-eslint/eslint-plugin
.eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
  ],
  settings: {
    react: {
      version: 'detect',
    },
  },
  rules: {
    '@typescript-eslint/explicit-function-return-type': [
      'error',
      {
        allowTypedFunctionExpressions: true,
      },
    ],
    '@typescript-eslint/indent': ['error', 2],
    'react/prop-types': 'off',
  },
  overrides: [
    {
      files: ['*.js'],
      rules: {
        '@typescript-eslint/no-var-requires': 'off',
      },
    },
  ],
};

CSS Modules

Let us now add the ability to style our components using the style-loader and css-loader.

npm i -D css-loader style-loader
config/webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const config = {
  
  // ...entry, resolve

  module: {
    rules: [
      {
        test: /\.(ts)x?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/i,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
            },
          },
        ],
      },
    ],
  },
  
  // ...plugins
};

module.exports = config;
src/types/css-modules.d.ts

Without this, you’ll be getting the Cannot find module <module name> error.

declare module '*.css' {
  const styles: { [className: string]: string };
  export default styles;
}

We can now import css file that will be applied globally

src/global.css
@import url('https://fonts.googleapis.com/css?family=Muli:400,800&display=swap');

body {
  margin: 0;
  font-family: 'Muli', sans-serif;
}
src/index.tsx
import React from 'react';
import { render } from 'react-dom';
import App from './App';
import './global.css'

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

Or you could write a locally scoped css style like the example below

src/App.css
.app {
  display: flex;
  background-color: #0a3d62;
  flex-direction: column;
  font-size: xx-large;
  text-align: center;
  text-shadow: 2px 3px rgba(0, 0, 0, 0.4);
  justify-content: center;
  align-items: center;
  height: 100vh;
}
src/App.tsx
import React, { FC } from 'react';
import Title from './components/Title';
import styles from './App.css';

const App: FC = () => (
  <div className={styles.app}>
    <Title title="React + Webpack + Babel 7 + Typescript" />
  </div>
);

export default App;

Or even write inline styles

import React, { FunctionComponent } from 'react';

interface Props {
  title: string;
}

const Title: FunctionComponent<Props> = ({ title }) => (
  <h1 style={{ color: '#f9ca24' }}>{title}</h1>
);

export default Title;

You should now have a page that looks something like this:

2 Comments

  1. Hi!

    Your setup looks interesting. Do you have the source code hosted in github so your readers can play around with it themselves?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.