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 xkcd.com/2347/ .
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 en.wikipedia.org/wiki/Waterbed_theory .
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 | jakelazaroff.com 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. jakelazaroff.com/words/no-one-ever-got-fired-for-choosing-react/ . 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 esbuild.github.io ! It’s a batteries-included JS and CSS bundler made by Evan Wallace Made by Evan madebyevan.com/ , co-founder and former CTO of Figma. Written in Go rather than JavaScript, it’s ridiculously fast esbuild - FAQ esbuild.github.io/faq/#benchmark-details , and it powers some other popular bundlers you may have heard of Vite Next Generation Frontend Tooling vitejs.dev/guide/dep-pre-bundling.html .
Goals
What features do we want when building a web app? Here’s my opinionated list:
- A development server with live reload: I work significantly faster when the browser is already up-to-date with my latest changes by the time I look over from my text editor.
- JavaScript and CSS bundling: This lets me structure files however I want, since the tooling will combine them to produce the final output that runs in the browser.
- Syntax lowering: There are a lot of cool new JavaScript and CSS features, and it’s nice to be able to use them before they’re widely supported.
- TypeScript support: This is a must for me, but if you don’t like TypeScript just forget I said anything!
- CSS modularity: It’s convenient if the styles in different components are isolated from each other. Some people use Tailwind for this, but I don’t like it Tailwind is a Leaky Abstraction | jakelazaroff.com Although Tailwind does have some benefits, ultimately it’s just one more thing to learn. jakelazaroff.com/words/tailwind-is-a-leaky-abstraction/ . My preferred way to solve this problem is CSS modules GitHub - css-modules/css-modules: Documentation about css-modules Documentation about css-modules. Contribute to css-modules/css-modules development by creating an account on GitHub. github.com/css-modules/css-modules .
- Speed: I suppose this is a nice-to-have, but development is so much more efficient (and fun!) when there’s a quick feedback loop between making a change and seeing the results.
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. www.npmjs.com/package/next?activeTab=dependencies , and Packagephobia reports a 170MB install size next - Package Phobia Find the size of next: The React Framework - Package Phobia packagephobia.com/result?p=next . 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 packagephobia.com/result?p=esbuild 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... github.com/evanw/esbuild/releases/tag/v0.18.14 . 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
src/
├─ 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>
<html>
<head>
<link rel="stylesheet" href="/index.css" />
</head>
<body>
<div id="app"></div>
<script src="/index.js"></script>
</body>
</html>
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! www.joshwcomeau.com/css/custom-css-reset/ , 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 esbuild.github.io/api/#live-reload :
new EventSource("/esbuild").addEventListener("change", () => location.reload());
Implementation
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 \
--inject:src/livereload.js
This builds atop my TIL on using esbuild to run a dev server with live reload til/esbuild/run-a-development-server-with-live-reload.md at main · jakelazaroff/til A collection of useful things I've learned. Contribute to jakelazaroff/til development by creating an account on GitHub. github.com/jakelazaroff/til/blob/main/esbuild/run-a-development-server-with-live-reload.md . Let’s go through what each line does:
esbuild src/index.html src/index.tsx
runs esbuild withsrc/index.html
andsrc/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 esbuild.github.io/content-types/#copy files ending in.html
unchanged to thebuild
folder. This also gets triggered when the HTML file changes.--outdir=build --bundle
tells esbuild to bundle all the files and place them in thebuild
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 thebuild
folder--serve-fallback=src/index.html
servessrc/index.html
instead of a 404 page (useful for client-side routing)--inject:src/livereload.js
appendssrc/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:
--sourcemap
makes debugging easier by exporting.js.map
and.css.map
files that tell the browser how your bundled code relates to your source code.--loader
lets you import different file types from JS and CSS. A particularly useful option is thefile
loader, which copies the file to the build directory and embeds the file name as a string. For example, to be able to useurl()
to load.woff2
files from a stylesheet, you’d add--loader:.woff2=file
.--jsx-factory
lets you change how JSX is compiled into function calls if you’d rather replace React with a different library like Preact.- This isn’t a flag, but in your
tsconfig.json
Documentation - What is a tsconfig.json Learn about how a TSConfig works www.typescriptlang.org/docs/handbook/tsconfig-json.html (orjsconfig.json
jsconfig.json Reference View the reference for jsconfig.json. code.visualstudio.com/docs/languages/jsconfig if you don’t use TypeScript) you can setpaths
to{ "~/*": ["./src/*"] }
in order to use~
to reference your files relative to the source root. For example, you’d importsrc/components/App.tsx
withimport App from "~/components/App"
.
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 mastodon.social/@jakelazaroff !
Updates
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... github.com/evanw/esbuild/releases/tag/v0.19.0 , that’s the default behavior, so that flag is no longer necessary!
Footnotes
-
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. developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog 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. developer.chrome.com/blog/tether-elements-to-each-other-with-css-anchor-positioning/ 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. developer.chrome.com/docs/web-platform/view-transitions/ will let us animate page navigation with just HTML and CSS. Among others! ↩ -
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. www.npmjs.com/package/esbuild?activeTab=dependencies 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. github.com/evanw/esbuild/blob/main/go.mod . ↩ -
It was actually possible to get CSS modules before with esbuild plugins esbuild - Plugins esbuild.github.io/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. github.com/mhsdesign/esbuild-plugin-lightningcss-modules that uses Devon Govett’s excellent Lightning CSS Lightning CSS An extremely fast CSS parser, transformer, bundler, and minifier. lightningcss.dev . But every extra dependency and config file adds friction, which is why I try to minimize them when building something simple. ↩
-
This example assumes we’re using TypeScript. If you don’t want to use TypeScript, just change the
.tsx
extensions to.jsx
and delete thattypes.d.ts
file. ↩ -
It probably goes without saying, but that one dependency is esbuild. ↩