DEV Community

Paul Allies
Paul Allies

Posted on • Edited on

Webpack, React, Typescript, React Hot Loader

So in this post I show how to configure your next react project to use React Typescript and Webpack.

Initialise Project

Create project folder, npm init, git init and open project in vs code.

mkdir myproject && cd myproject && yarn init -y && git init && code .
Enter fullscreen mode Exit fullscreen mode

Create an appropriate .gitignore file

node_modules
dist
Enter fullscreen mode Exit fullscreen mode

Install Webpack Dev Dependencies and Configure Webpack

To run webpack bundler we need the webpack tools

yarn add -D webpack webpack-cli
Enter fullscreen mode Exit fullscreen mode

Let's add a script in our package.json

{
  "name": "myproject",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^4.44.0",
    "webpack-cli": "^3.3.12"
  }
}

Enter fullscreen mode Exit fullscreen mode

When you run "yarn build", there will be an error message:

ERROR in Entry module not found: Error: Can't resolve './src' 
Enter fullscreen mode Exit fullscreen mode

Let's create an index.js in a "src" folder

src/index.js

console.log("Hello");
Enter fullscreen mode Exit fullscreen mode

Let's run "yarn build" and see that a "dist/main.js" was created. Webpack without config looks for a "src/index.js" file and compiles to a "dist/main.js". To further control the configuration of webpack we need to create a webpack.config.js

webpack.config.js

const path = require("path");

module.exports = {
    entry: "./src/index.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js"
    }
}
Enter fullscreen mode Exit fullscreen mode

Create Home page

To run the bundle.js file within the browser, we need an index.html page. Create one in the src folder: "src/index.html"

src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Note: We've purposely left out the src reference. We'll use webpack to inject the compiled bundle.js the file. To do this install the HtmlWebpackPlugin. Webpack plugins are utilities used after compilation.

yarn add -D html-webpack-plugin
Enter fullscreen mode Exit fullscreen mode

Update the webpack.config.js file to include the plugin

webpack.config.js

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

module.exports = {
    entry: "./src/index.js",
    output: {
        filename: "bundle.js"
    },
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html",
        hash: true, // This is useful for cache busting
        filename: '../index.html'
    })]
}
Enter fullscreen mode Exit fullscreen mode

After build you'll now notice that a "dist/index.html" was created which includes the link to the bundle.js file

dist/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  <script src="bundle.js?d645258de977f9a9c7c4"></script></body>
</html>

Enter fullscreen mode Exit fullscreen mode

Let's configure our app for react.

Install React

yarn add react react-dom
Enter fullscreen mode Exit fullscreen mode

Let's change our "src/index.js":

src/index.js

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

const root = document.getElementById("root");
render(<App />, root);
Enter fullscreen mode Exit fullscreen mode

src/App.js

import React from "react";

const App = () => {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    const interval = setInterval(() => {
      setCount((count) => count + 1);
    }, 500);
    return () => clearInterval(interval);
  }, []);

  return <h2>Count: {count}</h2>;
};

export default App;
Enter fullscreen mode Exit fullscreen mode

If you build again you'll get an error:

ERROR in ./src/index.js 6:16
Module parse failed: Unexpected token (6:16)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| 
| const rootEl = document.getElementById('root');
> ReactDOM.render(<App />, rootEl);

Enter fullscreen mode Exit fullscreen mode

For webpack to compile react we use Babel. So install the babel dependencies

yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader
Enter fullscreen mode Exit fullscreen mode

To use babel we need a config file ".babelrc.js"

module.exports = {
    presets: ["@babel/preset-react", "@babel/preset-env"]
}
Enter fullscreen mode Exit fullscreen mode

Here we inform babel to use the env preset and the react preset.

The @babel /preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

Once again we need to update the webpack.config.js file

webpack.config.js

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

module.exports = {
    entry: "./src/index.js",
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            }
        ]
    },
    output: {
        filename: "bundle.js"
    },
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html",
        hash: true, // This is useful for cache busting
        filename: '../index.html'
    })]
}
Enter fullscreen mode Exit fullscreen mode

After running build we now can run the application from dist.

Install Typescript

yarn add -D typescript @types/react @types/react-dom awesome-typescript-loader
Enter fullscreen mode Exit fullscreen mode

To use typescript we need a tsconfig.json file. Generate on with

tsc --init
Enter fullscreen mode Exit fullscreen mode

Edit the tsconfig.json to allow typescript to handle react jsx. Change "jsx": "preserve" to "jsx": "react"

tsconfig.json

{
  "compilerOptions": {
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
    "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
    "strict": true /* Enable all strict type-checking options. */,
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
    "skipLibCheck": true /* Skip type checking of declaration files. */,
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  }
}


Enter fullscreen mode Exit fullscreen mode

Update the webpack.config.js file

webpack.config.js

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

module.exports = {
    mode: "development",
    devtool: "source-map",
    entry: './src/index.tsx',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            },
            {
                test: /\.(ts|tsx)?$/,
                loader: "awesome-typescript-loader",
                exclude: /node_modules/
            },
        ]
    },
    resolve: {
        extensions: ['.ts', '.js', '.json', ".tsx"]
    },
    output: {
        filename: "bundle.js"
    },

    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html",
        hash: true, // This is useful for cache busting
        filename: 'index.html'
    })]
}
Enter fullscreen mode Exit fullscreen mode

Rename all react files to *.tsx extension and build app again. Update App.tsx with typescript code

import React from "react";

const App = () => {
  const [count, setCount] = React.useState<number>(0);

  React.useEffect(() => {
    const interval = setInterval(() => {
      setCount((count) => count + 1);
    }, 500);
    return () => clearInterval(interval);
  }, []);

  function displayCount(message: string): string {
    return message;
  }

  return <h2>{displayCount(`Count: ${count}`)}</h2>;
};

export default App;

Enter fullscreen mode Exit fullscreen mode

Lastly setup a dev environment to hot reload our changes as we update our components during coding.

Install web dev server and react-hot-load

yarn add react-hot-load
yarn add -D webpack-dev-server
Enter fullscreen mode Exit fullscreen mode

Update webpack.config.js for web-dev-server and hot reload

webpack.config.js

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

module.exports = {
    mode: "development",
    devtool: "source-map",
    entry: './src/index.tsx',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            },
            {
                test: /\.(ts|tsx)?$/,
                loader: "awesome-typescript-loader",
                exclude: /node_modules/
            },
        ]
    },
    resolve: {
        extensions: ['.ts', '.js', '.json', ".tsx"]
    },
    output: {
        filename: "bundle.js"
    },

    devServer: {
        port: 4000,
        open: true,
        hot: true
    },

    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html",
        hash: true, // This is useful for cache busting
        filename: 'index.html'
    })]
}
Enter fullscreen mode Exit fullscreen mode

Update your App.tsx file for hot reload

src/App.js

import { hot } from "react-hot-loader";
import React from "react";

const App = () => {
  const [count, setCount] = React.useState<number>(0);

  React.useEffect(() => {
    const interval = setInterval(() => {
      setCount((count) => count + 1);
    }, 500);
    return () => clearInterval(interval);
  }, []);

  function displayCount(message: string): string {
    return message;
  }

  return <h2>{displayCount(`Testing Count: ${count}`)}</h2>;
};

export default hot(module)(App);
Enter fullscreen mode Exit fullscreen mode

Lastly to run the web dev server in dev, add the a script

package.json

...
  "scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
...

Enter fullscreen mode Exit fullscreen mode

Run "yarn dev", update some code in App.tsx and watch your changes hot reload without losing state.

Cheers for now!

Top comments (1)

Collapse
 
davehax profile image
Davehax

To anyone reading this a year on- please replace the following in webpack.config.js:

  • 'awesome-typescript-loader' with 'ts-loader'
  • 'webpack-dev-server' with 'webpack serve'

I just followed this guide and after a bit of banging my head against the desk managed to resolve some issues that popped up while following this tutorial, seemingly caused by package upgrades over time.

Posting the package.json at the time of writing the article would have helped with this as well.