Skip to main content

One post tagged with "build"

View All Tags

Minimal React Frontend

· 5 min read

To accelerate being UI development and debugging of Snowscape, I'm building a web-based front-end that can connect to the engine. I'm familiar with React and TypeScript, so being able to use the browser development environment will speed things along (versus trying to learn egui or another Rust-based framework at this point in development).

JavaScript tooling drives me a bit bonkers so here's a quick write-up on my "minimal" setup to get a front-end going (i.e. for simple, non-production purposes). Given the rate of change in the JavaScript ecosystem, who knows how long this post will be useful or relevant for!

The files

The "minimal" files needed here are:

src/
app.tsx
main.tsx
index.html
style.css
scripts/
open-browser.js

.gitignore
package.json
Makefile

The Makefile (Makefile)

I like Makefiles because I know make build is going to build my project and I don't have to worry about whether the project is using npm, npx, tsc, esbuild, cargo, etc. This is great for large complex monorepos using multiple languages as well as for coming back to old personal projects where I've long since forgotten all the details of how I build it.

I'm a big fan of language agnostic command-runners and Make, while hardly perfect, is ubiquitously available -- which is a good fit for a command runner you're using to avoid having to remmeber specialized tools.

.PHONY: ensure build run dev

ensure:
npm i

build: ensure
mkdir -p dist/static
cp src/index.html dist
cp src/style.css dist
npx esbuild \
--preserve-symlinks \
--loader:.js=jsx \
--loader:.md=text \
--loader:.yaml=text \
--loader:.txt=text \
--sourcemap \
--bundle src/main.tsx \
--outfile=dist/main.bundle.js

run: build
(sleep 2 && node scripts/open-browser.js) &
npx http-server -c-1 dist

dev:
$(MAKE) run &
npx nodemon \
--watch src \
--ext ts,tsx,html,css,yaml,yml \
--exec "make build || exit 1"

Tools & dependencies (package.json)

The above requires some tools, so let's look at the package.json:

{
"devDependencies": {
"@types/react": "^18.3.5",
"esbuild": "^0.23.1",
"http-server": "^14.1.1",
"nodemon": "^3.1.4",
"react-dev-utils": "^12.0.1"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}

open-browser.js

And we have one super simple script scripts/open-browswer.js for opening a browswer tab when the client is launched. It's basically just a wrapper to call a function in an external package:

const openBrowser = require('react-dev-utils/openBrowser');
openBrowser('http://localhost:8080');

Files to keep out of git (.gitignore)

And let's not forget about making a .gitignore so we don't accidentally commit built files to the repo that we don't want there:

/node_modules/
/dist/

Boilerplate minimal HTML (index.html)

We need an index.html to host the page:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="cache-control" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
<link href="style.css" rel="stylesheet" />
<title></title>
</head>
<body>
<div id="root"></div>
<script src="main.bundle.js" type="application/javascript"></script>
</body>
</html>

Boilerplate CSS (style.css)

I generally use inline CSS for small projects, but it's nice to have a single CSS for global settings, normalization, etc.

body {
margin: 0;
padding: 0;

font-family: monospace;
}

React bootstrapping (main.tsx)

I like the convention of having a main() function that calls a Main component and, between those two, all the foundational "plumbing" of a React app is handled. In particular, for a simple development/debugging client like it adds very basic, minimal "hot reloading" that polls for changes to the underlying script bundle (no complicated dev servers: just a simple polling loop with a handful of lines of code).

import React, { JSX } from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './app';

function Main(): JSX.Element {
return <App />;
}

async function main() {
console.log('--- snowscape client main ----');
pollForReload('/main.bundle.js');
pollForReload('/style.css');

const element = document.getElementById('root')!;
const root = ReactDOM.createRoot(element);
root.render(<Main />);
}

main();

function pollForReload(url) {
let previous: string | null = null;
const poll = async () => {
const resp = await fetch(url);
const text = await resp.text();
if (previous === null) {
previous = text;
} else if (previous !== text) {
window.location.reload();
}
setTimeout(poll, 800 + Math.random() * 800);
};
setTimeout(poll, 250);
}

The actual app (App.tsx)

The plumbing out of the way, we now have a place to start developing the app in a file that free of the any of the bootstrapping logic:

import React, { JSX } from 'react';

export function App(): JSX.Element {
return (
<div>
<h1>Hello App</h1>
</div>
);
}

Where's all the other stuff?

What about eslint, hot reloading dev server that can load individual components, tailwind CSS integration, API generation, deployment scripts, etc. etc.?

I'm very wary of the "not built here" syndrome that leads to developers (including myself) building things themselves that have already been built by others in high-quality fashion. However, over the years, too many times JavaScript build systems have "locked" my projects into a certain way of developing that (1) makes upgrades to new libraries hard, (2) doesn't work with other libraries / tools, (3) breaks mysteriously after not being used for 6+ months, (4) etc. As such, I tend to try to keep JavaScript build systems pretty minimal and "unabstracted" so it's easier to debug when there's a build issue. That said, the above is "good enough" for most of the simple one-off apps I experiment with but certainly not the best and probably not what is desirable for a full, production web app being developed by a team!