React, TypeScript, Tailwind CSS and Webpack 5 starter

Starter template using React 18, TypeScript, TailwindCSS, and Webpack 5, optimized for seamless development and efficient production builds.

webpack

This article provides a step-by-step guide to setting up an environment using React, TypeScript, and Tailwind CSS, bundled with Webpack 5. By the end, we’ll have a streamlined setup with hot module replacement, source maps, and type-checking. Our production bundle will be optimized to efficiently purge unused CSS, minify code, and leverage code splitting for superior performance.

View Final Project

Prerequisites

Before starting, ensure you have Node.js v21.x or later and npm installed.

Setting Up the Project

mkdir react-typescript-tailwindcss-webpack5-starter cd react-typescript-tailwindcss-webpack5-starter npm init -y

TypeScript and Babel

In this project, we'll leverage TypeScript for type-checking, enhancing our code quality, maintainability, and productivity. Then, we'll use Babel to transpile our JavaScript, ensuring cross-browser compatibility and support for modern features.

Install TypeScript

npm i -D typescript

In the root of the project, create the tsconfig.json file to define the TS compiler options.

{ "compilerOptions": { "noEmit": true, "target": "ESNext", "lib": ["dom", "dom.iterable", "ESNext"], "module": "ESNext", "moduleResolution": "node", "jsx": "react-jsx", "declaration": true, "outDir": "dist", "allowJs": true, "checkJs": false, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "allowSyntheticDefaultImports": true, "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "types": ["node", "@testing-library/jest-dom"] }, "ts-node": { "transpileOnly": true, "files": true, "compilerOptions": { "module": "CommonJS" } }, "include": ["src/**/*"] } /* TypeScript configuration options: - "noEmit": true - We're using Babel for compilation, do not emit outputs. - "baseUrl": "." - Base directory to resolve non-absolute module names. - "target": "esnext" - Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', or 'ESNEXT'. - "lib": ["dom", "dom.iterable", "esnext"] - Comprehensive type support for modern web development. - "module": "esnext" - Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. - "moduleResolution": "node" - Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). - "jsx": "react-jsx" - Specify JSX code generation: 'preserve', 'react-native', or 'react'. - "declaration": true - Generates corresponding '.d.ts' file. - "outDir": "dist" - Redirect output structure to the directory. - "allowJs": true - Allow JavaScript files to be compiled. - "checkJs": false - Enable error reporting in type-checked JavaScript files. - "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'. - "allowSyntheticDefaultImports": true - Allow default imports from modules with no default export. This does not affect code emit, just typechecking. - "isolatedModules": true - Ensure that each file can be safely transpiled without relying on other imports. - "esModuleInterop": true - Emit '__importDefault' instead of 'require' for ES modules. - "skipLibCheck": true - Skip type checking of all declaration files (*.d.ts). - "paths": { "@/*": ["src/*"] - Sets up path mapping for module resolution. } - "types": ["node", "@testing-library/jest-dom"] - Specifies types to include. ts-node configuration options: - "transpileOnly": true - Skip type checking for faster compilation. - "files": true - Include files that are specified in the "include" array. - "compilerOptions": { "module": "commonjs" - Use CommonJS module system for Node.js compatibility. } - "include": [] - Specifies files to include in the project. */

We'll complete the TypeScript setup during the Webpack configuration.


Install Babel

Babel is a JavaScript compiler that lets us use ECMAScript 2015+ code while ensuring backward compatibility with older browsers. Let's install the necessary plugins for our project.

  1. Install Babel core to use the next-generation JS features.
    npm i -D @babel/core

  2. Install Babel preset env to use the latest JS without needing to micromanage.
    npm i -D @babel/preset-env

  3. Install Babel Preset for React to compile JSX and other React-specific syntax.
    npm i -D @babel/preset-react

  4. Install Babel Preset for TypeScript to allow Babel to compile TypeScript code.
    npm i -D @babel/preset-typescript

  5. Install Babel Loader to allow Webpack to process files using Babel.
    npm i -D babel-loader

  6. Create the .babelrc config file in the root of the project to include presets and plugins.

    • @babel/preset-env: "targets": "defaults": Specifies target environments for transpilation, ensuring broad compatibility with common browsers and environments.
    • @babel/preset-react: Transpiles JSX and React syntax. "runtime": "automatic": Enables new JSX Transform in React 17, allowing JSX without importing React.
    • @babel/preset-typescript: Purpose: Transpiles TypeScript code to JavaScript.
{ "presets": [ ["@babel/preset-env", { "targets": "defaults" }], ["@babel/preset-react", { "runtime": "automatic" }], "@babel/preset-typescript" ] }

We'll complete the Babe setup during the Webpack configuration.


Install Tailwind CSS

Tailwind CSS is a utility-first CSS framework that promotes rapid UI development and design consistency by offering a flexible and customizable approach to styling.

  1. npm i -D tailwindcss postcss autoprefixer

    1. tailwindcss: Tailwind CSS framework
    2. postcss: Optimizes the CSS build process by using plugins for tasks like autoprefixing, minification, and linting.
    3. autoprefixer adds vendor prefixes based on the values from Can I use.
  2. npx tailwindcss init

    1. Initializes a new Tailwind CSS configuration file to add your customizations, define a theme, extend the default Tailwind configuration, etc.
  3. Install purgecss to remove unused selectors from our css for our production build. npm i -D @fullhuman/postcss-purgecss

  4. Create ./postcss.config.ts in the root to add Tailwind to our PostCSS configuration.

module.exports = { plugins: { // Include the Tailwind CSS plugin. tailwindcss: {}, // Add vendor prefixes to CSS rules. autoprefixer: {}, // Add PurgeCSS plugin only in production mode. ...(process.env.NODE_ENV === 'production' && { '@fullhuman/postcss-purgecss': { /* Specify the paths to all of the template files in the project. */ content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], // Function to extract class names from the content. defaultExtractor: (content: string) => /* Match all words, hyphens, and colons that are not followed by another colon */ content.match(/[\w-/:]+(?<!:)/g) || [], }, }), }, };
  1. Configure your tailwind.config.ts - Check this guide to create your own.
module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { extend: {}, }, plugins: [], };
  1. Add the Tailwind directives to your CSS to: src/styles/index.css
@tailwind base; @tailwind components; @tailwind utilities;

We'll complete the Tailwind setup during the Webpack configuration.


Install React, React DOM, and their types

  1. npm i react react-dom
  2. npm i -D @types/react @types/react-dom
  3. Create the React entry point.
react-typescript-tailwind-webpack5-starter ├── public/ │ ├── index.html <body> <div id="root"></div> </body> ├── src/ │ ├── index.tsx └── package.json

index.tsx

import React from 'react'; import ReactDOM from 'react-dom/client'; import './styles/index.css'; // Tailwind CSS const root = ReactDOM.createRoot(document.getElementById('root')!); root.render( <React.StrictMode> <div>hello world</div> </React.StrictMode> );

Webpack 5

We're using Webpack to bundle our code and to streamline our development environment. This includes setting up a development server with hot module replacement and source maps for effective debugging. For the production build, we'll optimize our code by minimizing it, removing unused CSS, and implementing efficient code splitting to boost performance and load times. We'll organize our configuration into three files: one for common settings, one for development, and one for production.

  1. webpack.common.ts: Common configuration shared by both development and production.
  2. webpack.dev.ts: Development-specific configuration.
  3. webpack.prod.ts: Production-specific configuration.
react-typescript-tailwind-webpack5-starter ├── webpack/ │ ├── webpack.common.ts │ ├── webpack.dev.ts │ └── webpack.prod.ts ├── src/ └── package.json
  1. Install Webpack, CLI, and the dev server.

    • Webpack bundles our application files.
    • webpack-cli provides a command-line interface for running Webpack.
    • webpack-dev-server provides a dev server. npm i -D webpack webpack-cli webpack-dev-server
  2. Install webpack-merge to combine multiple Webpack configuration files. npm i -D webpack-merge

  3. Install ts-loader allows Webpack to process .ts and .tsx files.
    npm i -D ts-loader

  4. Install plugin-transform-runtime to re-use of Babel's injected helper code.
    npm i -D @babel/plugin-transform-runtime

  5. Install css-loader to import and bundle CSS in our JavaScript files.
    npm i -D css-loader

  6. Install postcss-loader to process CSS with PostCSS.
    npm i -D postcss-loader

  7. Install mini-css-extract-plugin to extract our CSS into separate files.
    npm i -D mini-css-extract-plugin

  8. Install html-webpack-plugin to inject the bundles into the HTML file.
    npm i -D html-webpack-plugin

  9. Install clean-webpack-plugin to clean the output directory before each build.
    npm i -D clean-webpack-plugin

  10. Install css-minimizer-webpack-plugin to optimize and minimize the CSS files.
    npm i -D css-minimizer-webpack-plugin

  11. Install file-loader to handle the importing of files into our JS modules.
    npm i -D file-loader

  12. Install ts-node to run TS code directly without needing to pre-compile it to JS first. This is handy for configuration files written in TS.
    npm i -D ts-node

  13. Install compression-webpack-plugin to prepare compressed versions of assets.
    npm i -D compression-webpack-plugin

  14. Install webpack-bundle-analyzer to visualize size of the webpack output files.
    npm i -D webpack-bundle-analyzer @types/webpack-bundle-analyzer


Webpack Common Configuration

The commonConfig file defines the base configuration that will be shared across both development and production environments.

  • Entry: Specifies the entry point for your application.
  • Output: Define where and how the bundled files should be output.
    1. path: Specifies the output directory as dist in the current directory.
    2. filename: Names output files using the entry name and a content hash for cache busting.
    3. chunkFilename: Names the chunk files (non-entry files) similarly with a content hash.
    4. assetModuleFilename: Specifies the naming convention for asset files, placing them in an assets directory with a hash.
    5. clean: Cleans the output directory (dist) before each build to remove old files.
  • Resolve Extensions: Specifies the file extensions that Webpack should resolve.
  • Loaders: Module.Rules Defines how different types of files should be processed.
    1. test: Regex pattern to match file types.
    2. use: Specifies the loader to use.
    3. exclude: Excludes specified directories.
  • Plugins: Enhances and customizes Webpack’s build capabilities.

Common Configuration

import path from 'path'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import webpack from 'webpack'; const commonConfig: webpack.Configuration = { context: path.resolve(__dirname, '../'), // Entry point for the application entry: './src/index.tsx', output: { // Output directory for bundled files path: path.resolve(__dirname, '../dist'), // Naming convention for the output files filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].bundle.js', // Naming convention for asset files assetModuleFilename: 'assets/[name].[hash][ext][query]', // Clean the output directory before each build clean: true, }, resolve: { // File extensions to resolve extensions: ['.js', '.jsx', '.ts', '.tsx'], // Aliases for module imports alias: { '@': path.resolve(__dirname, '../src'), }, }, module: { rules: [ { // Use Babel loader for JS and TS files test: /\.[jt]sx?$/, use: 'babel-loader', exclude: /node_modules/, }, { // CSS modules support configuration test: /\.module\.css$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { modules: { localIdentName: '[name]__[local]-[hash:base64:5]', }, }, }, 'postcss-loader', ], }, { // Regular CSS configuration test: /\.css$/, exclude: /\.module\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', ], }, { // File loader for images and other assets test: /\.(png|jpg|gif|svg|webp)$/, use: [ { loader: 'file-loader', options: { name: 'assets/[path][name].[hash:base64:5].[ext]', context: 'src', }, }, ], }, ], }, plugins: [ // Plugin to generate HTML file new HtmlWebpackPlugin({ template: './public/index.html', }), // Plugin to clean the output directory new CleanWebpackPlugin(), // Plugin to extract CSS into separate files new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash].css', chunkFilename: 'css/[id].css', ignoreOrder: false, }), ], }; export default commonConfig;

Development Configuration

import { merge } from 'webpack-merge'; import commonConfig from './webpack.common'; import webpack from 'webpack'; import path from 'path'; import 'webpack-dev-server'; const devConfig: webpack.Configuration = { // Set the mode to development for better debugging and performance mode: 'development', // Enable inline source maps for easier debugging devtool: 'inline-source-map', devServer: { // Serve static files from the public directory static: { directory: path.join(__dirname, '../public'), }, // Enable hot module replacement hot: true, // Automatically open the browser after the server starts open: true, // Set the port for the development server port: 3000, }, // Add the HotModuleReplacementPlugin for hot reloading plugins: [new webpack.HotModuleReplacementPlugin()], }; // Merge the common configuration with the development-specific configuration export default merge(commonConfig, devConfig);

Production Configuration

import { merge } from 'webpack-merge'; import commonConfig from './webpack.common'; import TerserPlugin from 'terser-webpack-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import webpack from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import CompressionPlugin from 'compression-webpack-plugin'; // Check if bundle analysis is enabled const isAnalyze = process.env.ANALYZE === 'true'; const prodConfig: webpack.Configuration = { // Set the mode to production for optimizations mode: 'production', // Generate source maps for debugging devtool: 'source-map', optimization: { // Enable minimization of the output files minimize: true, minimizer: [ // Use TerserPlugin for JavaScript minimization new TerserPlugin({ terserOptions: { compress: { // Remove console statements drop_console: true, }, output: { // Preserve license comments comments: /@license|@preserve/i, }, }, }), // Use CssMinimizerPlugin for CSS minimization new CssMinimizerPlugin({ minimizerOptions: { preset: [ 'default', { // Remove all comments from CSS discardComments: { removeAll: true }, }, ], }, }), ], // Split chunks for better caching and performance splitChunks: { chunks: 'all', cacheGroups: { // Separate React and related libraries into a vendor chunk reactVendor: { test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/, name: 'vendor-react', chunks: 'all', priority: 30, enforce: true, }, // Separate other vendor libraries into a vendor chunk vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor-libs', chunks: 'all', priority: 20, reuseExistingChunk: true, enforce: true, }, // Group common modules into a common chunk common: { test: /[\\/]src[\\/]/, name: 'common', chunks: 'all', minChunks: 2, priority: 10, reuseExistingChunk: true, enforce: true, }, }, }, }, plugins: [ // Define environment variables for production new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), }), // Compress assets using gzip new CompressionPlugin({ filename: '[path][base].gz', algorithm: 'gzip', test: /\.(js|css|html|svg)$/, threshold: 8192, minRatio: 0.8, }), // Optionally analyze the bundle size ...(isAnalyze ? [ new BundleAnalyzerPlugin({ analyzerMode: 'static', logLevel: 'info', defaultSizes: 'gzip', generateStatsFile: true, reportFilename: 'bundle-analyzer.html', }), ] : []), ], }; // Merge the common configuration with the production-specific configuration export default merge(commonConfig, prodConfig);

Finally, update your package.json scripts to use the new configuration files:

"scripts": { "start": "webpack serve --config webpack.dev.ts", "build": "webpack --config webpack.prod.ts", "analyze": "ANALYZE=true npm run build" }

How to use

  1. To start the development server:
    npm run start
  2. To build the project for production:
    npm run build
  3. To analyze the bundle size to identify potential optimizations. npm run analyze

Summary

This combo configuration is designed to streamline React prototyping and is well-suited for vanilla React projects. It is flexible to scale up and adapt to your project's needs. In the final setup, I added Prettier and ESLint for code formatting and linting, ensuring consistent code quality and style across the project. Additionally, I included Jest and React Testing Library for unit testing, making sure everything works just right.

View Final Project