Reactful.ts

The always-recent guide to creating a development environment for Node and React (with TypeScript and Webpack)

You can count on this guide to always be recent. We update it after any major change in any of the packages used. The instructions here should work on any machine with a recent version of Nodejs (>= 16). We recommend using the latest LTS release of Node.

This is a step-by-step guide with explanations about all the tools used. At the end of the guide, there is "sample" server-render-ready application that you can use to test your configurations.

This guide uses tools that are popular in the JavaScript ecosystem. Express (for a web server), Webpack (for a module bundler), and TypeScript (for a JS/JSX compiler). If you want to use Babel instead of TypeScript, checkout jscomplete.com/reactful

After going through these instructions, you will have a simple but fully-configured environment that’s ready for rendering React applications both on the front-end and the back-end (for server-side rendering). The instructions start with an empty directory and they will guide you through all the dependencies that you need, one-by-one. There will be no use of any "code-generation" tools like create-react-app.

1. Initializing

Create an empty directory and initialize it with a package.json file. This file is used in Nodejs projects to store general information about the project (like its name, version, etc) and track what dependencies the project needs (so that anyone can install them all at once).

You can create this file using the npm init command:

$ mkdir reactful

$ cd reactful

$ npm init

The npm init command will ask you a few questions and you can interactively supply your answers (or press Enter to keep the defaults).

You can use npm init -y to generate your package.json file with the default values that npm can detect about your project (the y is for yes to all questions).

Once the npm init command is done, you should have a package.json file under your project directory (and nothing else, yet).

The package.json file can now be used to "document" any dependencies you need to add to your project. This happens automatically when you npm install anything.

2. Installing main dependencies

A JavaScript environment has 2 types of dependencies, main dependencies that need to be installed on all environments, and development dependencies that are only needed on local development machines.

For a Nodejs web server, one popular option to use is Express. You can use it to serve dynamic content under your web server. (You can also use Express to serve static content, but you should consider using a better option for that, like NGINX or a CDN service.)

To add Express to the project:

$ npm i express

This command will download the express npm package and place it under a node_modules folder (which it will create because express is the first package we’re adding). The command will also save this dependency to your package.json file.

The i in the command above is just a shortcut for install.

The frontend dependencies you need are React and ReactDOM. Add them next:

$ npm i react react-dom
While the react and react-dom packages are not really needed in production because they get bundled into a single file, this guide assumes that you deploy your unbundled code to production and bundle things there. If you want to bundle things in development and push your bundled files to production, you can install these packages - and most of what’s coming next - as development dependencies.

Since you’ll be writing your code in multiple modules (files) and it will depend on other modules (like React), you need a module bundler to translate all these modules into something that can work in all browsers today. You can use Webpack for that job. The packages you need to install now are:

$ npm i webpack webpack-cli
The webpack-cli package provides the webpack command, which you can use to bundle your modules. The actual Webpack core code is hosted separately under the webpack packages.

Webpack is just a generic module bundler. You need to configure it with loaders to transform code from one state into the other. For example, you need to transform React’s JSX code into React’s API calls. We’re going to use TypeScript to do that. Besides compiling JSX, TypeScript also transforms modern JavaScript features into code that can be understood in any execution environment, and gives you an environment where you can make your code type-safe.

Here are the packages you need to make TypeScript do its magic:

$ npm i typescript ts-loader

The typescript package has the core features of TypeScript, and the ts-loader package provides the loader that Webpack needs to work with TypeScript files.

3. Installing development dependencies

The following are dependencies that are not needed in production. To track them separately, you can use the npm -D install flag to save them under a devDependencies section in package.json.

When you run a Node server and then change the code of that server, you need to restart Node. This will be a frustrating thing in development. Luckily, there are some workarounds. A good option for a TypeScript project is ts-node-dev:

$ npm i -D ts-node-dev

This package will make the tsnd command available in your project. That command runs your Node server in a wrapper process that monitors the main process and automatically restarts it when files are saved to the disk. Simple and powerful!

Another priceless development dependency is ESLint. DO NOT SKIP THIS ONE!

ESLint is a code quality tool and if you don’t use it, your code will not be as good as it could be.

Since TypeScript is part of this stack, you need to configure ESLint to parse through what TypeScript is going to parse through. You should also use the main recommended ESLint configurations in addition to those recommended for React projects. Here are the packages you need for that:

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

To configure ESLint, you need to add a .eslintrc.js file in the root of the project. This file will naturally depend on your code style preferences, but definitely start it with the recommended configurations and then customize them as needed:

.eslintrc.js
module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    project: "tsconfig.json",
    sourceType: "module",
  },
  env: {
    browser: true,
    es6: true,
    jest: true,
    node: true,
  },
  plugins: [
    "eslint-plugin-react",
    "eslint-plugin-react-hooks",
  ],
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
  ],
  settings: {
    "react": {
      version: "detect",
    },
  },
  rules: {
    "react/prop-types": "off",
    "react/react-in-jsx-scope": "off",

    // You can do more rule customizations here...
  },
};
You should configure your editor to highlight any ESLint issues for you as you type. All the major editors today have plugins to do that. You should also make your editor auto-format code for you on save. Prettier is a great option for that and it works well with ESLint.

The most popular testing library that’s usually used with React is Jest. You’ll also need ts-jest to make jest work with TypeScript files:

$ npm i -D jest ts-jest

4. Creating an initial directory structure

This really depends on your style and preferences, but a simple structure would be something like:

reactful/
  dist/
    main.js
  src/
    index.tsx
    components/
      app.tsx
    server/
      server.tsx

Note how I am using the .tsx extension for files under src. This signals to TypeScript that these files might include the JSX syntax. By using this file extension, we don’t need to do any further configuration for compiling JSX!

Use conventions over configurations when you can. It’s not just about using the conventions of a fraework (or library). You should make your own code leverage this very powerful concepts.

Note how I created a separate server directory for the backend code. It’s always a good idea to separate code that you run in your trusted private backends from code that is to be run on public clients.

5. Configuring Webpack and TypeScript

To configure TypeScript to compile JSX and modern JavaScript code, create a tsconfig.js file under the root of the project and put the following module.exports object in it:

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "downlevelIteration": true,
    "lib": ["dom", "es2021", "scripthost"],
    "jsx": "react-jsx",
    "allowJs": false,
    "sourceMap": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "preserveSymlinks": true,
    "resolveJsonModule": true
  },
  "include": ["src"]
}

To configure Webpack to bundle your application into a single bundle file, create a webpack.config.js file under the root of the project and put the following module.exports object in it:

webpack.config.js
module.exports = {
  entry: "./src/index.tsx",
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "ts-loader",
        },
      },
    ],
  },
};
By default, Webpack outputs the bundle to dist/main.js. If you want to change that, you’ll need to configure the output property in the config object.

6. Creating npm scripts for development

You need 2 commands to run this environment. You need to run your web server and you need to run Webpack to bundle the frontend application for browsers. You can use npm scripts to manage these.

You can also use the Webpack Dev Server instead of manually running the webpack command, but while that works great in development it’s not a good idea in production. I like my development environment to be as close as possible to the production environment.

In your package.json file, there is a scripts section. If you generated the file with the npm init defaults you’ll have a placeholder "test" script in there. Change that to work with Jest:

// In package.json
scripts: {
  "test": "jest"
}

Now, add 2 more scripts in there. The first script is to run the server file with tsnd. You can name the script anything. For example:

  "dev:server": "tsnd --files --respawn src/server/server.tsx --ignore-watch node_modules,dist"
It’s probably a good idea to ignore the dist/ directory when restarting Node automatically as changes in the dist/ directory are driven by changes in the src/ directory, which is already monitored.

The other script that you need is a simple runner for Webpack:

  "dev:bundler": "webpack -w --mode=development"

The -w flag in the command above is to run Webpack in watch mode as well and the --mode=development flag is to make Webpack generate a development-friendly bundle.

Run Webpack with --mode=production in production.

Here’s the whole scripts section in package.json after adding these 2 new dev tasks (note the commas):

// In package.json
scripts: {
  "test": "jest",
  "dev:server": "tsnd --files --respawn src/server/server.tsx --ignore-watch node_modules,dist",
  "dev:bundler": "webpack -w --mode=development"
}

7. Testing everything with a sample application

At this point, you are ready for your own code. If you followed the exact configurations above, you’ll need to place your ReactDOM.render call (or .hydrateRoot for SSR code) in src/index.tsx and serve dist/main.js in your root HTML response.

Here is a sample server-side ready React application that you can test with:

src/components/app.tsx
import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      This is a sample stateful and server-side
      rendered React application.
      <br />
      <br />
      Here is a button that will track
      how many times you click it:
      <br />
      <br />
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
}
src/index.tsx
import ReactDOM from "react-dom/client";

import App from "./components/app";

const container = document.getElementById("app");
ReactDOM.hydrateRoot(container, <App />);
src/server/server.tsx
import express from "express";
import ReactDOMServer from "react-dom/server";

import App from "../components/app";

const server = express();
server.use(express.static("dist"));

server.get("/", (req, res) => {
  const initialMarkup = ReactDOMServer.renderToString(<App />);

  res.send(`
    <html>
      <head>
        <title>Sample React App</title>
      </head>
      <body>
        <div id="app">${initialMarkup}</div>
        <script src="/main.js"></script>
      </body>
    </html>
  `)
});

server.listen(4242, () => console.log("Server is running..."));

That’s it. If you run both npm dev:server and dev:bundler scripts (in 2 separate terminals):

$ npm run dev:server
$ npm run dev:bundler

Then open up your browser on http://localhost:4242/, you should see the React application rendered. This application should also be rendered if you disable JavaScript in your browser!