DEV Community

Simon Boisset
Simon Boisset

Posted on • Edited on • Originally published at simonboisset.com

Create react app with esbuild

How to configure esbuild to create a react app

Esbuild is a new javascript bundler. It's written with Go and is extremely fast. Let's go to use it to create react with hot reload app from scratch without webpack

You can check the code on this repos.

Initialization

Create your folder project and initialize it.

yarn init
Enter fullscreen mode Exit fullscreen mode
{
  "name": "esbuild-static",
  "version": "1.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Install dependencies

yarn add esbuild dotenv react react-dom styled-components
Enter fullscreen mode Exit fullscreen mode

Then add devdependencies.

yarn add --dev typescript @types/react @types/react-dom @types/styled-components @types/node serve-handler @types/serve-handler
Enter fullscreen mode Exit fullscreen mode

Typescript config

Add tsconfig.json file.

{
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "module": "commonjs",
    "target": "ESNext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "moduleResolution": "node",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "jsx": "react"
  },
  "include": ["src"],
  "exclude": ["**/node_modules", "**/.*/"]
}
Enter fullscreen mode Exit fullscreen mode

Esbuild config

Create esbuild folder then add dev.js and prod.js files.

The dev config watch files changes and start a server for hot reload and static files. You can add environment variables too.

const { spawn } = require('child_process');
const esbuild = require('esbuild');
const { createServer, request } = require('http');
require('dotenv').config();
const handler = require('serve-handler');

const clientEnv = { 'process.env.NODE_ENV': `'dev'` };
const clients = [];

Object.keys(process.env).forEach((key) => {
  if (key.indexOf('CLIENT_') === 0) {
    clientEnv[`process.env.${key}`] = `'${process.env[key]}'`;
  }
});

const openBrowser = () => {
  setTimeout(() => {
    const op = { darwin: ['open'], linux: ['xdg-open'], win32: ['cmd', '/c', 'start'] };
    if (clients.length === 0) spawn(op[process.platform][0], ['http://localhost:3000']);
  }, 1000);
};

esbuild
  .build({
    entryPoints: ['src/index.tsx'],
    bundle: true,
    minify: true,
    define: clientEnv,
    outfile: 'dist/index.js',
    sourcemap: 'inline',
    watch: {
      onRebuild(error) {
        setTimeout(() => {
          clients.forEach((res) => res.write('data: update\n\n'));
        }, 1000);
        console.log(error || 'client rebuilt');
      },
    },
  })
  .catch((err) => {
    console.log(err);
    process.exit(1);
  });

esbuild.serve({ servedir: './' }, {}).then((result) => {
  createServer((req, res) => {
    const { url, method, headers } = req;
    if (req.url === '/esbuild') {
      return clients.push(
        res.writeHead(200, {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache',
          'Access-Control-Allow-Origin': '*',
          Connection: 'keep-alive',
        }),
      );
    }

    const path = url.split('/').pop().indexOf('.') ? url : `/index.html`;
    const proxyReq = request({ hostname: '0.0.0.0', port: 8000, path, method, headers }, (prxRes) => {
      res.writeHead(prxRes.statusCode, prxRes.headers);
      prxRes.pipe(res, { end: true });
    });
    req.pipe(proxyReq, { end: true });
    return null;
  }).listen(5010);

  createServer((req, res) => {
    return handler(req, res, { public: 'dist' });
  }).listen(3000);

  openBrowser();
});
Enter fullscreen mode Exit fullscreen mode
const esbuild = require('esbuild');
require('dotenv').config();

const clientEnv = { 'process.env.NODE_ENV': `'production'` };
for (const key in process.env) {
  if (key.indexOf('CLIENT_') === 0) {
    clientEnv[`process.env.${key}`] = `'${process.env[key]}'`;
  }
}
esbuild
  .build({
    entryPoints: ['src/index.tsx'],
    bundle: true,
    minify: true,
    define: clientEnv,
    outfile: 'dist/index.js',
  })
  .catch(() => process.exit(1));
Enter fullscreen mode Exit fullscreen mode

Eslint config

Install eslint.

yarn add --dev eslint eslint-config-react-app @typescript-eslint/eslint-plugin @typescript-eslint/parser
Enter fullscreen mode Exit fullscreen mode

Add .eslintrc.js file.

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ['react-app'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 13,
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Scripts

Add scripts to package.json

"scripts": {
    "build": "node esbuild/prod",
    "type-check": "tsc --noEmit",
    "lint": "eslint src/**/*.ts src/**/*.tsx",
    "start": "nodemon --watch dist --exec 'yarn type-check & yarn lint' & node esbuild/dev"
  },
Enter fullscreen mode Exit fullscreen mode

React app

In src folder add index.tsx file.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import GlobalStyle from './globalStyle';

ReactDOM.render(
  <>
    <GlobalStyle />
    <App />
  </>,
  document.getElementById('root'),
);
Enter fullscreen mode Exit fullscreen mode

Hot reload tools

For listening esbuild dev server reload we must add a hook for development.

import { useEffect } from 'react';

const useHMR = () => {
  useEffect(() => {
    if (process.env.NODE_ENV !== 'production') {
      new EventSource('http://localhost:5010/esbuild').onmessage = () => window.location.reload();
    }
  }, []);
};
export default useHMR;
Enter fullscreen mode Exit fullscreen mode

CSS with styled-components

Add global style with styled-components

import { createGlobalStyle } from 'styled-components';

 const GlobalStyle = createGlobalStyle`
  body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
`;
export default GlobalStyle
Enter fullscreen mode Exit fullscreen mode

App

Fanaly create the App component.

import React, { FC } from 'react';
import useHMR from './useHMR';
import Logo from './Logo';

const App: FC = () => {
  useHMR();
  return (
    <div className='App'>
      <header className='App-header'>
        <Logo />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a className='App-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer'>
          Learn React
        </a>
      </header>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Static files

Add static files in dist folder.

<!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="React App" />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
<script src="/index.js"></script>
Enter fullscreen mode Exit fullscreen mode

Then create other files : favicon.icomanifest.jsonlogo192.png

Run

Start dev server.

yarn start
Enter fullscreen mode Exit fullscreen mode

Build for production

yarn build
Enter fullscreen mode Exit fullscreen mode

Now let's go to code

Top comments (6)

Collapse
 
dj0024javia profile image
Dhaval Javia • Edited

Great Article @simonboisset ! I must say i haven't found any article with detailed explaination of esbuild with react.

I did check all of your examples are those were super helpful. However, i did find a bug where in case of dynamic routing in react, which is mainly handled by react-router, it navigates properly if you go from / route to /home.

Issue arises when you try to refresh on /home, as it will throw 404 not found error.
for dev.js -> Change this line
const path = url.split('/').pop().indexOf('.') ? url :/index.html;

into following snippet and it will work just fine for dynamic routing in react with esbuild.

const path = url.split('/').pop().indexOf('.') > -1 ? url :/index.html;

Sumitted PR in your repo for the same.

Cheers

Collapse
 
simonboisset profile image
Simon Boisset

Hey @dj0024javia ! Thanks for your PR. I'm glad that my article can help you.

Collapse
 
barouchmaxime profile image
barouchmaxime

Great Article, exactly what I was looking for, Tx a lot for sharing, wanted to try it and access the link repo but it is not working, 404 not found, would you please make it accessible, it is easier to try it with the repo, it will be very helpful, for all users, Thank you very much.

Collapse
 
simonboisset profile image
Simon Boisset

Thanks for your feedback and sorry for the broken link. I updated it and it should works now.

Collapse
 
barouchmaxime profile image
barouchmaxime

Thank you very much Simon, finally yesterday I was able to follow the step in the article and make the livereload work, I was sooo happy :), I am really sure the link you provided will also be very useful for many others. The step of the article are clear enough, and after a few copy/paste in my own project, I was able to make it work. I tried other articles with no success, and I really wanted to thank you for posted this article, that help a lot to progress in this learning journey :)

Thread Thread
 
simonboisset profile image
Simon Boisset

Thank you for your feedback. I am happy that my article can be useful for you