Setup React with Typescript and esbuild.
Tags: webdev, react, typescript, esbuild
Published: 2021-08-24 14:39:27

When we want to develop a React app the number one choice is [Create React App (CRA)](https://create-react-app.dev/), it is a complete framework ready to develop and ship your app, but this is the Javascript ecosystem and always gonna be a bunch of alternatives so one of them can be a development template similar to CRA based on Typescript and esbuild.

What is esbuild? you ask, well esbuild is like it's [homepage](https://esbuild.github.io/) describes it: **"An extremely fast JavaScript bundler"** and this is true, go to the homepage to checkout the benchmarks.

**DISCLAIMER: this guide has the purpose to show how you can setup React, Typescript and esbuild as modules bundler, so you can use it in small projects, if this is not your case I strongly recomend use CRA.**

Said that, let's go to write some code lines. First checkout the folder structure:
![folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ue4s9uc9i6zgwjy74b69.png)

As you see it, within this folder structure are the tipical folders public and src, like CRA **src folder** contains an entry point `index.tsx` this one is gonna be used by esbuild to generate the bundles, also includes another files that I explain bellow, the **public folder** contains the `index.html` that is used by the development server, the **esbuild folder** contains the files `serve.ts` and `build.ts` that creates the development server and builds the app respectively also includes a config file used by both files, the rest files are config files used by **eslint** and **Jest** (yes, this template also includes the popular test runner). Before dive into each folder and their respectives files checkout the `package.json` and `tsconfig.json`.

## `package.json`
```json
"scripts": {
    "type-check": "tsc",
    "start": "yarn type-check && ts-node esbuild/serve",
    "build": "yarn type-check && ts-node esbuild/build",
    "test": "yarn type-check && jest"
  },
  "dependencies": {
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "esbuild": "^0.12.21",
    "open": "^8.2.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "ts-node": "^10.2.1",
    "typescript": "^4.1.2"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.15.0",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.15.0",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^12.0.0",
    "@testing-library/user-event": "^13.2.1",
    "@types/jest": "^26.0.15",
    "babel-jest": "^27.0.6",
    "eslint": "^7.32.0",
    "eslint-plugin-jest-dom": "^3.9.0",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-react": "^7.24.0",
    "eslint-plugin-react-hooks": "^4.2.0",
    "eslint-plugin-testing-library": "^4.11.0",
    "jest": "^27.0.6"
  }
```
These are all the dependencies you need to make this template works succesfully, maybe you found unfamiliar the **open** package, this one is gonna be used by `serve.ts` to open your default browser, the rest are tipical dependencies you find within a React-Typescript app. As follow, there are the **scripts** field, the `type-check` script as you guess is used to run the Typescript compiler before the another scripts. The rest scripts are related with the folders mentioned previously and are gonna be explain each other bellow.

## `tsconfig.json`

```json
{
  "ts-node": {
    "extends": "ts-node/node14/tsconfig.json",
    "transpileOnly": true,
    "files": true,
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
      "esModuleInterop": true,
      "moduleResolution": "node"
    }
  },
  "compilerOptions": {
    "target": "es6",
    "baseUrl": "src",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}
``` 
About `tsconfig.json` the field **compilerOptions** setups the Typescript compiler when the `type-check` script runs, **ts-node** field setups the ts-node package this one allows execute the scripts `start` and `build`. Now, checkout the other scripts:

## `start`
This script executes the `serve.ts` file, this file uses the `esbuild.serve()` method as follow: 

```typescript
function startDevServer() {
  createServer(async (...args) => {
    const res = args[1];
    try {
      const publicHTML = await readFile(join(PUBLIC_PATH, "index.html"), { encoding: "utf-8" });
      res.end(publicHTML);
    } catch (error) {
      console.log(error);
    }
  }).listen(DEV_SERVER_PORT, () => {
    console.log(`Development server is now running at ${DEV_SERVER_URL}`);
  });
}

(async () => {
  const server = await serve(serveOptions, transformOptions);
  const { host: HOST, port: PORT } = server;

  console.log("ESBuild is now serving your files at:");
  console.table({ HOST, PORT });

  startDevServer();
  await open(DEV_SERVER_URL);
})();
```
First an IIFE is called, then the `serve` method is called, this method creates a local server that serves the bundled files (js, css and static files) based on **serveOptions** and **transformOptions**. This objects are provide by the **config** file mentioned previously. 

### `serveOptions`
```typescript
export const serveOptions: ServeOptions = {
  servedir: "www",
  host: "127.0.0.1",
  port: 8080,
};
```
`serveOptions` sets the server, this is http://localhost:8080. 

### `transformOptions`
```typescript
export const transformOptions: BuildOptions = {
  entryPoints: ["src/index.tsx"],
  outdir: "www/serve",
  bundle: true,
  format: "esm",
  inject: ["esbuild/config/react-shim.ts"],
  loader: serveLoader,
};
```
`transformOptions` sets esbuild that outputs the bundles at URL: http://localhost:8080/serve, this object has two keys, **inject** and **loader**. **inject** uses the file `react-shim.ts` this file allows auto import React:

### `react-shim.ts`
```typescript
import * as React from "react";
export { React };
```
**loader** uses the object `serveLoader`, this loader sets esbuild to process static files as "dataurl" at development, the other option is process static files as "file" but is more convenient serve files as "dataurl".

```typescript
const serveLoader: ILoader = {
  ".png": "dataurl",
  ".jpg": "dataurl",
  ".webp": "dataurl",
  ".jpeg": "dataurl",
  ".gif": "dataurl",
  ".svg": "dataurl",
};
```
Based on the entry point file extension esbuild knows that have to process jsx syntax. 

`ServeOptions` and `TransformOptions` are types provide by esbuild, ILoader is a type based on `Loader` type (also provide by esbuild).

### `ILoader`
```typescript
type ILoader = {
  [key: string]: Loader;
};
```
Until now the template is serving files at http://localhost:8080/serve, open this URL at your browser.

![localhost:8080](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7bilzae58gun7q0kt4u3.png)

With this in mind, we can create an `index.html` file at public folder that consumes the files at http://localhost:8080/serve as follow:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web app created using React and ESbuild" />
    <link rel="manifest" href="manifest.json" />
    <!--
      Styles sheets provide by your React app are serve by the developement server running at http://localhost:8080/
      this server is created by Esbuild when executes the "start" script.
    -->
    <link rel="stylesheet" href="http://localhost:8080/serve/index.css" />
    <title>React ESbuild template with Typescript</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      The JS files are serve same way that the style sheets are.
    -->
    <script src="http://localhost:8080/serve/index.js" type="module"></script>
  </body>
</html>
```
Now only left serve `index.html`, the function `startDevServer` at `serve.ts` takes care of this, first initializates a http server at http://localhost:3000, then reads the `index.html` and sends this one on each request.

![start script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dbfgymc5quh3grk97gzn.gif)

Well done! Now we can develop react apps, you only to do is reload your browser to view the changes you do.

## `build`
The `build` script executes the `build.ts` file as follow: 

```typescript
import {
  PUBLIC_PATH,
  buildOptions,
  DEV_LINK_TAG,
  DEV_SCRIPT_TAG,
  BUILD_LINK_TAG,
  BUILD_SCRIPT_TAG,
  HTML_COMMENTS,
} from "./config";

const { readFile, writeFile, copyFile } = promises;

async function createHTMLFileAtBuildPath() {
  await copyFile(join(PUBLIC_PATH, "favicon.ico"), join("build", "favicon.ico"));
  await copyFile(join(PUBLIC_PATH, "manifest.json"), join("build", "manifest.json"));
  await copyFile(join(PUBLIC_PATH, "robots.txt"), join("build", "robots.txt"));

  const HTMLFileAtPublicPath = await readFile(join(PUBLIC_PATH, "index.html"), {
    encoding: "utf-8",
  });
  const HTMLFileAtBuildPath = HTMLFileAtPublicPath.replace(
    HTML_COMMENTS,
    "<!--Files generate by ESbuild-->"
  )
    .replace(DEV_LINK_TAG, BUILD_LINK_TAG)
    .replace(DEV_SCRIPT_TAG, BUILD_SCRIPT_TAG);

  writeFile(join("build", "index.html"), HTMLFileAtBuildPath, { encoding: "utf8" });
  console.log("Your build has been created succesfully");
}

buildSync(buildOptions);
createHTMLFileAtBuildPath();
```
First imports some constants from config, these are used to process the `index.html` file at build time.

```typescript
export const DEV_SERVER_PORT = 3000;
export const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
export const PUBLIC_PATH = "public";
export const HTML_COMMENTS = /<!--[\s\S]*?-->/g;
export const DEV_LINK_TAG = `<link rel="stylesheet" href="http://localhost:8080/serve/index.css" />`;
export const DEV_SCRIPT_TAG = `<script src="http://localhost:8080/serve/index.js" type="module"></script>`;
export const BUILD_LINK_TAG = `<link rel="stylesheet" href="index.css">`;
export const BUILD_SCRIPT_TAG = `<script src="index.js" type="module"></script>`;
```
Then `esbuild.buildSync()` method is called, it processes the `src/index.tsx` file based on **buildOptions** object and outputs the generate bundles at build folder.

```typescript
export const buildOptions: BuildOptions = {
  entryPoints: ["src/index.tsx"],
  outdir: "build",
  bundle: true,
  sourcemap: true,
  minify: true,
  format: "esm",
  inject: ["esbuild/config/react-shim.ts"],
  target: ["es6"],
  loader: buildLoader,
};
``` 
**buildOptions** uses a diferent loader, this is because at build time the static files are output at build folder and pointed by esbuild in this path.

```typescript
const buildLoader: ILoader = {
  ".png": "file",
  ".jpg": "file",
  ".webp": "file",
  ".jpeg": "file",
  ".gif": "file",
  ".svg": "file",
};
```

After `esbuild.buildSync` runs `createHTMLFileAtBuildPath()` is called, first copies the files from public path to build path, then replaces the `index.html` developent tags by build tags and writes the new `index.html` at build folder.

### `index.html` at build folder

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web app created using React and ESbuild" />
    <link rel="manifest" href="manifest.json" />
    <!--Files generate by ESbuild-->
    <link rel="stylesheet" href="index.css">
    <title>React ESbuild template with Typescript</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--Files generate by ESbuild-->
    <script src="index.js" type="module"></script>
  </body>
</html>
```
To run the **start** and **build** scripts without any kind of issue we need to add some files at src folder. First a `env.d.ts` this file allows us use external modules or files eg: the spinning React logo is a **.svg** file if we not declare this extension Typescript marks as an error, the solution is simple declare ".svg" file at `.env.d.ts`.

 ```typescript
declare module "*.svg" {
  const content: any;
  export default content;
}
```
You can declare all the external files or modules that you need. Another file we need is `jest-setup.ts` which allows add some global config like auto import react and testing-library/jest-dom assertions.

```typescript
import "@testing-library/jest-dom";
import * as React from "react";
window.React = React; // Auto import React
```
### `test`
This template is incomplete if does not include a test runner, as I mentioned later, the files `jest.config.ts` and `.babelrc` are for setup **Jest**. These files:

### `jest.config.ts`
```typescript
import type { Config } from "@jest/types";

const config: Config.InitialOptions = {
  verbose: true,
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["<rootDir>/src/jest-setup.ts"],
  transform: {
    "^.+\\.[t|j]sx?$": "babel-jest",
  },
  moduleNameMapper: {
    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
      "<rootDir>/src/__mocks__/file-mock.ts",
    "\\.(css|less)$": "<rootDir>/src/__mocks__/style-mock.ts",
  },
};

export default config;
```
### `.babelrc`
```json
{
  "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}
```
Also we must create a **__mocks__** folder at src for mocking css and external files see moduleNameMapper at `jest.config.ts`

### `__mocks__/styles-mock.ts`

```typescript
export {};
```

### `__mocks__/file-mock.ts`

```typescript
export default "test-file-stub";
```
Nice! You can run your components tests.
![running test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2eowjv3i5qds9wvtnlta.gif)

Of course eslint is also include at this template.

### `.eslintrc`

```json
{
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:jsx-a11y/recommended",
    "plugin:testing-library/react",
    "plugin:jest-dom/recommended"
  ],
  "parserOptions": {
    "sourceType": "module",
    "ecmaVersion": "latest"
  },
  "env": { "browser": true, "es6": true, "jest": true },
  "rules": {
    "react/react-in-jsx-scope": "off",
    "react/prop-types": ["enabled", { "ignore": "ignore", "customValidators": "customValidator" }]
  }
}
```

And that's it, to develop React apps apart CRA all we need is a modules bundler, and esbuild is a powerful, flexible and faster one. You can find the entire code at [Github](https://github.com/jleonardo007/react-esbuild-template-ts-version) and go deep at implementation details. Hope this guide results useful for you. 

##Caveats
When you change any file at src folder `esbuild.serve()` refresh automatically files at http://localhost:8080/serve but you need to refresh your browser to see the new changes at your app.