An Extremely Simple React Starter Kit

Let’s face it: the modern JavaScript ecosystem has a reputation for complexity. People feel like best practices and popular tools change out from underneath them, there are too many layers of dependencies and everything is perpetually on the brink of collapse Dependency .

For the record, I think that take lacks nuance. We expect more out of web apps than ever before: real-time updates and collaboration, fancy animations, interactive widgets like popovers and nested dropdown menus — and accessible and performant versions of all of the above.1 The ecosystem is complex because building UIs is complex, and if we push down complexity in one area it’ll probably just pop up in another Waterbed theory - Wikipedia .

Still, though — the point is well taken. When I want to quickly try out an idea, time spent on tooling is time wasted No One Ever Got Fired for Choosing React | If you spend a lot of time on Hacker News, it’s easy to get taken by the allure of building a project without a framework. . I don’t want to have to deal with a bunch of configuration or read up on changes to a big framework or make sure different dependencies play well together. I want something boring that’s easy to spin up with as few moving parts as possible.

That’s why I feel compelled to write about one of my favorite web development tools: esbuild esbuild - An extremely fast bundler for the web ! It’s a batteries-included JS and CSS bundler made by Evan Wallace Made by Evan , co-founder and former CTO of Figma. Written in Go rather than JavaScript, it’s ridiculously fast esbuild - FAQ , and it powers some other popular bundlers you may have heard of Vite Next Generation Frontend Tooling .


What features do we want when building a web app? Here’s my opinionated list:

Using something like Next.js will get you all that and a lot more — but it’s much heavier. According to NPM, Next.js has 25 dependencies next The React Framework. Latest version: 13.4.10, last published: 5 days ago. Start using next in your project by running `npm i next`. There are 3664 other projects in the npm registry using next. , and Packagephobia reports a 170MB install size next - Package Phobia Find the size of next: The React Framework - Package Phobia . esbuild has one dependency,2 and adds a comparatively tiny 9MB esbuild - Package Phobia Find the size of esbuild: An extremely fast JavaScript and CSS bundler and minifier. - Package Phobia to your node_modules folder.

I should also mention that this list is for building a single-page app. There’s been a lot of debate around those lately, so let me reiterate: if you’re building a serious production app, you should probably use a framework that lets you render HTML on the server. This is about starting prototypes and fun projects quickly while keeping tooling to a bare minimum.

So why am I writing this now? As of a few days ago, esbuild added partial CSS module support Release v0.18.14 · evanw/esbuild Implement local CSS names (#20) This release introduces two new loaders called global-css and local-css and two new pseudo-class selectors :local() and :global(). This is a partial implementation o... . Before that, esbuild checked every one of those items except for CSS modularity; now, it’s got everything we need out of the box.3

Project Structure

We’ll go over a barebones example React app just to showcase the different features mentioned above. Here’s what the directory structure looks like:4

├─ components/
│  ├─ App.module.css
│  ╰─ App.tsx
├─ index.html
├─ index.tsx
├─ livereload.js
├─ style.css
╰─ types.d.ts

index.html is pretty straightforward: it sets up a basic HTML document, links to the bundled CSS and JS files and has an empty <div> for the React app to render into. Note that even though the global stylesheet in our source code is named style.css, we’re linking to index.css — esbuild names the bundled CSS file based on the name of the corresponding JS entrypoint.

<!DOCTYPE html>
    <link rel="stylesheet" href="/index.css" />
    <div id="app"></div>
    <script src="/index.js"></script>

index.tsx is similarly spartan. It has two jobs: import any global styles, and render the root React component.

import "./style.css";

import { createRoot } from "react-dom/client";

import App from "./components/App";

const app = document.querySelector("#app");
if (app) createRoot(app).render(<App />);

Global styles go in style.css. The selectors in there won’t be changed, so that’s a good place to put things that apply to the whole project: fonts, CSS reset My Custom CSS Reset I have a set of baseline CSS styles that come with me from project to project. In the past, I'd use a typical CSS reset, but times have changed, and I believe I have a better set of global styles! , custom properties, base styles, etc. You can also @import other stylesheets and they’ll be bundled along with it:

@import "./reset.css";

body {
  font-family: sans-serif;

App.tsx is the root component. The project layout at this point is kinda arbitrary; this is mostly a contrived example to show off CSS modules.

import css from "./App.module.css";

export default function App() {
  return <h1 className={css.wrapper}>Hello, world!</h1>;

Speaking of CSS modules, did you notice types.d.ts? That’s there because TypeScript doesn’t know how to type check .module.css files (so you can skip this section if you’re not using TypeScript). You need to tell it the type of the import:

declare module "*.module.css" {
  const map: { [key: string]: string };
  export default map;

Finally, there’s livereload.js, which will only be included in development. It (surprise!) reloads the page whenever the esbuild server rebuilds any files. This snippet is straight from the esbuild documentation on live reloading esbuild - API :

new EventSource("/esbuild").addEventListener("change", () => location.reload());


And now the big reveal: we can get the whole development environment with one dependency,5 two shell commands and zero config files!

Here’s the command for the dev server:

esbuild src/index.html src/index.tsx \
  --loader:.html=copy \
  --outdir=build --bundle --watch \
  --servedir=build --serve-fallback=src/index.html \

This builds atop my TIL on using esbuild to run a dev server with live reload til/esbuild/ at main · jakelazaroff/til A collection of useful things I've learned. Contribute to jakelazaroff/til development by creating an account on GitHub. . Let’s go through what each line does:

  • esbuild src/index.html src/index.tsx runs esbuild with src/index.html and src/index.tsx as entrypoints. We don’t need to specify any CSS files because esbuild will gather them as they’re imported into JS files.
  • --loader:.html=copy‌ tells esbuild to copy esbuild - Content Types files ending in .html unchanged to the build folder. This also gets triggered when the HTML file changes.
  • --outdir=build --bundle tells esbuild to bundle all the files and place them in the build folder.

Remember those flags, because we’ll use them both when developing locally and building for production. The rest of the flags are specific to development:

  • --watch tells esbuild to rebuild when any source files change.
  • --servedir=build serves all static files from the build folder
  • --serve-fallback=src/index.html serves src/index.html instead of a 404 page (useful for client-side routing)
  • --inject:src/livereload.js appends src/livereload.js to the end of the JS bundle.

Here’s how to build for production:

esbuild src/index.html src/index.tsx \
  --loader:.html=copy \
  --outdir=build --bundle --minify

It’s mostly the same! There are only two differences: the last two lines with the development server flags are gone, and --watch has been replaced with --minify.

If these commands seem kinda gnarly, remember that we don’t have to type out them every single time. They can go in the scripts section of our package.json file. I’ve consolidated this one a bit by moving the common flags into an esbuild script, and having start and build scripts add the extra flags at the end.

  "scripts": {
    "esbuild": "esbuild src/index.html src/index.tsx --loader:.html=copy --outdir=build --bundle",
    "start": "npm run esbuild -- --watch --servedir=build --serve-fallback=src/index.html --inject:src/livereload.js",
    "build": "npm run esbuild -- --minify"

Extra Credit

Okay, so we’ve covered our goals. But esbuild can do a lot more. Here are some other flags you might think of enabling:

That’s it! Happy hacking! If you come up with any cool additions to this, let me know jake lazaroff (@[email protected]) 147 Posts, 45 Following, 49 Followers · designer+developer+musician living in nyc !


Originally, this post recommended --loader:.module.css=local-css in order to use the local-css loader for .module.css files. As of esbuild v0.19.0 Release v0.19.0 · evanw/esbuild This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.js... , that’s the default behavior, so that flag is no longer necessary!


  1. The W3C has started chipping away at some of these. The new <dialog> element <dialog>: The Dialog element - HTML: HyperText Markup Language | MDN The <dialog> HTML element represents a dialog box or other interactive component, such as a dismissible alert, inspector, or subwindow. is a native way to build accessible modals, CSS anchor positioning Tether elements to each other with CSS anchor positioning - Chrome Developers A new API is coming to the web platform to help you position elements in an adaptive way with no tricks. looks to make things like popovers significantly easier and the View Transition API Smooth and simple transitions with the View Transitions API - Chrome Developers The View Transition API allows page transitions within single-page apps, and will later include multi-page apps. will let us animate page navigation with just HTML and CSS. Among others!

  2. Technically, NPM lists 22 dependencies esbuild An extremely fast JavaScript and CSS bundler and minifier.. Latest version: 0.18.14, last published: 2 days ago. Start using esbuild in your project by running `npm i esbuild`. There are 3172 other projects in the npm registry using esbuild. of esbuild, which is misleading — they’re all first-party shims for esbuild’s own native binaries, so from a JavaScript perspective you could say esbuild has zero! But esbuild is written in Go and although NPM can’t inspect it, if you look in the go.mod file you can see that the entire project only uses a single third-party dependency esbuild/go.mod at main · evanw/esbuild An extremely fast bundler for the web. Contribute to evanw/esbuild development by creating an account on GitHub. .

  3. It was actually possible to get CSS modules before with esbuild plugins esbuild - Plugins , such as this one GitHub - mhsdesign/esbuild-plugin-lightningcss-modules Contribute to mhsdesign/esbuild-plugin-lightningcss-modules development by creating an account on GitHub. that uses Devon Govett’s excellent Lightning CSS Lightning CSS An extremely fast CSS parser, transformer, bundler, and minifier. . But every extra dependency and config file adds friction, which is why I try to minimize them when building something simple.

  4. This example assumes we’re using TypeScript. If you don’t want to use TypeScript, just change the .tsx extensions to .jsx and delete that types.d.ts file.

  5. It probably goes without saying, but that one dependency is esbuild.