DEV Community

Cover image for The Complete Guide for Setting Up React App from Scratch (feat. TypeScript)
Brandon Wie
Brandon Wie

Posted on • Updated on

The Complete Guide for Setting Up React App from Scratch (feat. TypeScript)

with webpack, Babel, TypeScript, Sass, CSS Module, Tailwind, React Refresh(HMR)

In this tutorial, we're going to set up a React application without using create-react-app.
We're going to learn how to set up from scratch with TypeScript, Sass, Tailwind CSS, CSS module, absolute paths, and more.

TL;DR My GitHub Repository

Basic concepts you need to know beforehand

1. TSC does type checking, Babel does transpiling

  • TSC(TypeScript Compiler) that is included in the TypeScript package will only be used as a type checker of your source codes, and you can achieve it by setting:
{
    "compilerOptions": {
        ...
        "noEmit": true, /* Disable emitting files from a compilation. */
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

2. We're gonna try the Fast Refresh Feature

The Fast Refresh is a React Native feature that allows you to get near-instant feedback for changes in your React components However, my idol, Dan Abramov talked about applying it across the board in 2019 - as a replacement for purely userland solutions (like react-hot-loader).

If you navigate to the React Hot Loader page on npm, you'll see
React Hot Loader NPM page

as mentioned above, we're using the webpack; therefore, we're going to use @pmmmwh/react-refresh-webpack-plugin to apply the Fast Refresh feature to our app. I mean, it's for development, so what's the harm of trying it right?

3. What's up with all the CSS libraries such as CSS Module, Sass, and Tailwind CSS

I'm not saying you should use all of these libraries, but it's just to show you how to set up these for your future projects. In my case, I use Sass and CSS Module combination when I work on page components, and the Tailwind CSS when I need to create prototypes fast and when I make reusable small components.

Okay, enough with the prerequisites.
Let's begin.

Step 1. Create a folder for your project

$ mkdir my-app && cd my-app # create and go to my-app folder
Enter fullscreen mode Exit fullscreen mode

Step 2. Generate package.json file

$ yarn init
Enter fullscreen mode Exit fullscreen mode

then fill in the questions shown below following the prompt

yarn init

if you prefer to generate one based on your own defaults, use:

$ yarn init --yes # or '-y' in short
Enter fullscreen mode Exit fullscreen mode

if you don't have yarn installed please follow this link.

yarn init docs

You can definitely change the settings later on, so don't worry.

Step 3. Add React and React-related packages

$ yarn add react react-dom react-router-dom
$ yarn add --dev @types/react @types/react-dom @types/react-router-dom # for TypeScript
Enter fullscreen mode Exit fullscreen mode
  • react(v18.2.0) : The react package contains only the functionality necessary to define React components. It is typically used together with a React renderer like react-dom for the web, or react-native for the native environments.
  • react-dom(v18.2.0): This package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as react to npm.
  • react-router-dom(v6.4.4): contains bindings for using React Router in web applications.
  • @types/react, @types/react-dom, @types/react-router-dom from TypeScript

Step 4. Add webpack and webpack cli packages

with the Webpack, you can combine all of your React codes into one or more bundles, that are static assets including not only JavaScript or TypeScript files, but also Sass or Image files(png, jpg)

$ yarn add --dev webpack webpack-cli webpack-dev-server
$ yarn add --dev @types/webpack @types/webpack-dev-server # for TypeScript
Enter fullscreen mode Exit fullscreen mode
  • webpack(v5.75.0)
  • webpack-cli(v5.0.1): enables you to use the command-line interface of the Webpack
  • webpack-dev-server(v4.11.1)
  • @types/webpack, @types/webpack-dev-server: for TypeScript

Step 5. Add Babel core and preset for React packages

Babel is a compiler(or transpiler) and a toolchain that is mainly used to convert ECMAScript 2015+ code into a backward-compatible version of JavaScript in current and older browsers or environments. The preset-react package will convert JSX syntax to JavaScript codes that browsers can understand.

$ yarn add --dev @babel/core @babel/preset-env @babel/preset-react 
$ yarn add --dev @babel/preset-typescript # for TypeScript
$ yarn add --dev @babel/register # to use webpack.config.ts (TypeScript)
Enter fullscreen mode Exit fullscreen mode

Presets for Babel

  • @babel/preset-env(v7.20.2): allow you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s)
  • @babel/preset-react(v7.18.6): process and transform jsx syntax and display name
  • @babel/preset-typescript(v7.18.6)
    • since ts-loader for the webpack doesn't natively support HMR, I prefer Babel's preset to handle TypeScript, also because basically webpack works as a module bundler and Babel works as a compiler(transpiler). ts-loader documentation
  • @babel/register(v.7.18.9): The require hook will bind itself to node's require and automatically compile files on the fly. You need it when you want to use the webpack config file as TypeScript. If you use webpack.config.ts, and the webpack cli will complain

    Unable load /{path}/webpack.config.ts

    and

    Unable to use specified module loader for ".ts"

    if you don't have @babel/register. No need extra configurations.

webpack and Babel packages are added using --dev | -D flag not to include those in the final code bundle

Step 6. Create babel.config.json file

{
  // there's room for discussions, but I won't exclude node_modules here
  // https://stackoverflow.com/questions/54156617/why-would-we-exclude-node-modules-when-using-babel-loader
  // "exclude": "node_modules/**",
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      {
        "runtime": "automatic" // the default option, "classic" does not automatic import anything.
        // https://stackoverflow.com/questions/32070303/uncaught-referenceerror-react-is-not-defined
      }
    ],
    "@babel/preset-typescript" // transpile typescript
  ]
}

Enter fullscreen mode Exit fullscreen mode

Step 7. Add loaders for webpack

Disclaimer: these loaders are third-party packages maintained by community members, it potentially does not have the same support, security policy or license as webpack, and it is not maintained by webpack.
- webpack documentation

There are instructions below about implementations of the loaders listed below; however, you can go to the links and follow the instructions if you encounter any issues along the way.

$ yarn add --dev babel-loader # to connect Babel and webpack
$ yarn add --dev html-webpack-plugin html-loader # for HTML
$ yarn add --dev style-loader css-loader sass-loader postcss postcss-loader postcss-preset-env mini-css-extract-plugin # for CSS
$ yarn add --dev react-refresh @pmmmwh/react-refresh-webpack-plugin # for HMR
Enter fullscreen mode Exit fullscreen mode

# connect babel and webpack

# HTML-related

  • html-webpack-plugin(v5.5.0): generate an HTML5 file for you that includes all your webpack bundles in the body using script tags. You can use your own template as well.
  • html-loader(v4.2.0): exports HTML as string. HTML is minimized when the compiler demands.

# style-related

  • style-loader(v3.3.1): inject CSS into the DOM. It will be only used when it's on development. MiniCssExtractPlugin will be used for production.
    • Simply, the style-loader directly injects CSS inside style tags in the DOM, MiniCssExtractPlugin bundles your CSS and creates separate CSS files.
    • One of the reasons I decided to use as above is that when using the style-loader, it's imperative receiving unnecessary inline styles whereas you can load css files on demand when using MiniCssExtractPlugin. It doesn't make a huge difference if your application is relatively small thou.
  • css-loader(v6.7.2): interpret @import and url() like import/require() and will resolve them.

    You need the two loaders above even if you don't wanna use Sass or Tailwind

  • postcss-loader(v7.0.2): allow using postcss and to use Tailwind CSS.

    • w/ postcss(v8.4.19): a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
    • w/ postcss-preset-env(v7.8.3): convert modern CSS into something most browsers can understand, determining the polyfills you need based on your targeted browsers or runtime environments. It takes the support data that comes from MDN and Can I Use and determine from a browserlist whether those transformations are needed. It also packs Autoprefixer within and shares the list with it, so prefixes are only applied when you're going to need them given your browser support list.

Since we're going to use Sass and Tailwind CSS in addition to the native CSS, as recommended on the Tailwind's official documentation, we're going to use PostCSS. *You don't have to install autoprefixer because it is already included in the postcss-preset-env

  • sass-loader(v13.2.0): load a Sass/SCSS file and compiles it to CSS.

  • MiniCssExtractPlugin(v2.7.2): extract CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps. We're going to use it in production.

# HMR (Hot Module Replacement) related (development only)

What is 'Hot Module Replacement'

instead of using classic HotModuleReplacementPlugin, we're going to implement "Fast Refresh" with react-refresh and @pmmmwh/react-refresh-webpack-plugin.
TMI: Next.js natively supports the Fast Refresh

react-refresh implements the wiring necessary to integrate Fast Refresh into bundlers. Fast Refresh is a feature that lets you edit React components in a running application without losing their state. It is similar to an old feature known as "hot reloading", but Fast Refresh is more reliable and officially supported by React.

Disclaimer: @pmmmwh/react-refresh-webpack-plugin is not 100% stable

Step 8. Add extra

$ yarn add --dev typescript-plugin-css-modules # to use CSS module in TypeScript
Enter fullscreen mode Exit fullscreen mode

After everything is installed, your package.json should look something like this:

{
  "name": "my-app",
  "version": "0.0.1",
  "description": "Brandon's complete guide for manual react app setup with TypeScript ",
  "main": "./index.js",
  "author": "brandonwie",
  "license": "MIT",
  "private": false,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.4.5"
  },
  "devDependencies": {
    "@babel/core": "^7.20.5",
    "@babel/preset-env": "^7.20.2",
    "@babel/preset-react": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@babel/register": "^7.18.9",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
    "@types/react": "^18.0.26",
    "@types/react-dom": "^18.0.9",
    "@types/react-router-dom": "^5.3.3",
    "@types/webpack": "^5.28.0",
    "@types/webpack-dev-server": "^4.7.2",
    "babel-loader": "^9.1.0",
    "css-loader": "^6.7.2",
    "html-loader": "^4.2.0",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.7.2",
    "postcss": "^8.4.19",
    "postcss-loader": "^7.0.2",
    "postcss-preset-env": "^7.8.3",
    "react-refresh": "^0.14.0",
    "sass": "^1.56.2",
    "sass-loader": "^13.2.0",
    "style-loader": "^3.3.1",
    "tailwindcss": "^3.2.4",
    "typescript": "^4.9.3",
    "typescript-plugin-css-modules": "^4.1.1",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 9. Create tsconfig.json

$ touch tsconfig.json
# or you can let tsc to handle it
$ tsc --init
Enter fullscreen mode Exit fullscreen mode

it should look something like this:

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */
    /* Language and Environment */
    //NOTE @brandonwie target latest version of ECMAScript
    "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
    "lib": [
      "DOM",
      "DOM.Iterable",
      "ESNext"
    ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
    "jsx": "react-jsx" /* Specify what JSX code is generated. */,
    /* Modules */
    "module": "ESNext" /* Specify what module code is generated. */,
    //NOTE @brandonwie Search under node_modules for no-relative imports
    "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
    "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */,
    "paths": {
      "@*": ["*"]
    } /* Specify a set of entries that re-map imports to additional lookup locations. */,
    "types": [
      "node"
    ] /* Specify type package names to be included without being referenced in a source file. */,
    "resolveJsonModule": true /* Enable importing .json files. */,

    /* JavaScript Support */
    //NOTE @brandonwie Process & infer types from .js files
    "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
    "checkJs": true /* Enable error reporting in type-checked JavaScript files. */,
    /* Emit */
    //NOTE @brandonwie Don't emit; allow Babel to tranform files
    "noEmit": true /* Disable emitting files from a compilation. */,
    //NOTE @brandonwie Disallow features that require cross-file information for emit
    "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
    "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
    //NOTE @brandonwie Import non-ES modules as default imports
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
    //NOTE @brandonwie Enable strictest settings like strictNullChecks & noImplicitAny
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
    "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
    "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  },
  "include": ["src", "declaration.d.ts"] /* Include modules in the program. */,
  "exclude": ["node_modules", "build"] /* Exclude modules from the program. */,
  "plugins": [{ "name": "typescript-plugin-css-modules" }] // to use css modules
}

Enter fullscreen mode Exit fullscreen mode

Some of the settings above are personal; however, settings that aren't boolean are necessary.

Step 10. Create webpack.config.ts

$ touch webpack.config.ts
Enter fullscreen mode Exit fullscreen mode

it should look something like this:

import path from 'path';
import webpack from 'webpack';
import HTMLWebpackPlugin from 'html-webpack-plugin';
//create css file per js file: https://webpack.kr/plugins/mini-css-extract-plugin/
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';

const isDevelopment = process.env.NODE_ENV !== 'production';
// define plugins
const plugins: webpack.WebpackPluginInstance[] = [
  new HTMLWebpackPlugin({
    template: './public/index.html', // you have to have the template file
  }),
];
isDevelopment
  ? plugins.push(new ReactRefreshWebpackPlugin())
  : plugins.push(new MiniCssExtractPlugin());

const config: webpack.Configuration = {
  mode: isDevelopment ? 'development' : 'production',
  devServer: {
    hot: true,
    port: 3000,
  },
  entry: './src/index.tsx', // codes will be inside src folder
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'index.js',
    // more configurations: https://webpack.js.org/configuration/
  },
  plugins,
  resolve: {
    modules: [path.resolve(__dirname, './src'), 'node_modules'],
    // automatically resolve certain extensions (Ex. import './file' will automatically look for file.js)
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.scss', '.css'],
    alias: {
      // absolute path importing files
      '@pages': path.resolve(__dirname, './src/pages'),
    },
  },
  module: {
    rules: [
      {
        test: /\.html$/,
        use: ['html-loader'],
      },
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: require.resolve('babel-loader'),
            options: {
              plugins: [
                isDevelopment && require.resolve('react-refresh/babel'),
              ].filter(Boolean),
            },
          },
        ],
      },
      {
        test: /\.(sa|sc|c)ss$/i, // .sass or .scss
        use: [
          // Creates `style` nodes from JS strings
          'style-loader',
          // Translates CSS into CommonJS
          'css-loader',
          // for Tailwind CSS
          'postcss-loader',
          // Compiles Sass to CSS
          'sass-loader',
        ],
      },
    ],
  },
};

export default config;
Enter fullscreen mode Exit fullscreen mode

The code above is based on many other references, so I recommend you to play along with the settings when you finish the setup. Also, you can use .js file instead of .ts file and don't use @babel/register since I just wanted to try it with TypeScript.

Step 11. Create postcss.config.js

$ touch postcss.config.js
Enter fullscreen mode Exit fullscreen mode

it should look something like this:

module.exports = {
  // didn't add autoprefixer because it is already included in postcss-preset-env
  plugins: [require('tailwindcss'), require('postcss-preset-env')],
};

Enter fullscreen mode Exit fullscreen mode

Step 12. Create tailwind.config.js

$ touch tailwind.config.js
Enter fullscreen mode Exit fullscreen mode

it should look something like this:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

Enter fullscreen mode Exit fullscreen mode

Step 13. Create public folder and index.html file to use it as a template

From the setting above, we told the webpack to find ./public/index.html and use it as a template file

const plugins: webpack.WebpackPluginInstance[] = [
  new HTMLWebpackPlugin({
    template: './public/index.html', // you have to have the template file
  }),
];
Enter fullscreen mode Exit fullscreen mode
$ mkdir public && touch public/index.html
Enter fullscreen mode Exit fullscreen mode
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Brandon's App'</title>
  </head>
  <body>
    <div id="root"></div> <!-- for react -->
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

If you use VSCode, simply you can type ! and enter to create the HTML codes.

Step 14. Create src folder, index.tsx, and App.tsx

$ mkdir src && touch src/index.tsx src/App.tsx
Enter fullscreen mode Exit fullscreen mode

Your entry is set to index.tsx in the src folder in webpack.config.ts file

const config: webpack.Configuration = {
  mode: isDevelopment ? 'development' : 'production',
  devServer: {
    hot: true,
    port: 3000,
  },
  entry: './src/index.tsx', // codes will be inside src folder
  ...
}
Enter fullscreen mode Exit fullscreen mode

Your index.tsx file should look something like this:

import { createRoot } from 'react-dom/client';

import App from './App';

const container = document.getElementById('root');
const root = createRoot(container!);

root.render(<App />);
Enter fullscreen mode Exit fullscreen mode

If you've been using React version 17 or below, you may find the rendering method is somewhat different. Please follow the link, How to Upgrade to React 18.

Your App.tsx file may look something like this:

import styles from './App.module';
import SamplePage from '@pages/Sample';
import './main.css';

const App: React.FC = () => {
  return (
    <div>
      <div className={styles.title}>CSS module works!</div>
      <div className={styles.subtitle}>CSS module + Tailwind works!</div>
      <div
        className={
          'border-[10px] border-solid border-red-800 rounded-full w-[200px] h-[200px] flex items-center justify-center text-center'
        }
      >
        Tailwind works!
      </div>
      <SamplePage />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

I hope you get the ideas here. What we've been setting are now available in our codes.

🙏 Please visit my GitHub repository for more settings and codes.

Step 15. (optional) add scripts to package.json

  "scripts": {
    "start": "webpack serve --hot --mode development",
    "build": "webpack --mode production"
  }
Enter fullscreen mode Exit fullscreen mode

Now you can run the app with yarn start

This is what you'll see if you run the app.
main page

That's it for this post guys. Thank you so much for reading.

Next post will be about prettier and eslint settings using eslint-config-airbnb. See you next time! 👋🏼

References

Top comments (6)

Collapse
 
verasnp profile image
VerasNp

Hello, just wanted to congratulate you on the amazing post. It really solved many doubts that I had. One thing I had trouble with was tsconfig.json file. At first, the IDE was showing an error related to the esModuleInterop and allowSyntheticDefaultImports keys (TS1259: Module '"path"' can only be default-imported using the 'esModuleInterop' flag), but it stopped when I included the file webpack.config.ts in the include key of tsconfig.json. My question is, is it wrong to add webpack.config.ts in the include key?

Collapse
 
brandonwie profile image
Brandon Wie • Edited

Thanks for your comment. :)
Theoratically, I don't think it's wrong to add webpack.config.ts inside the include array.
Check out this link on Stackoverflow about using TypeScript as Webpack config.
Hope you find it helpful.

Happy Coding!

Collapse
 
sergsar profile image
sergsar

Thank you! This is the best guid I've found so far with Webpack, Typescript and SASS together

Collapse
 
brandonwie profile image
Brandon Wie

Hi @sergsar, thank you so much for your comment. I've been struggling for 3 days for this post :)
There will be some small updates of this post because I found some small issues while actually using this boiler template for my application.

Collapse
 
sergsar profile image
sergsar

Any suggestions with setup eslint and prettfier with @babel/preset-typescript would be great also

Collapse
 
brandonwie profile image
Brandon Wie

I'll work on it as soon as I get some of my tasks done. Happy Coding!