How I Build Frontend Apps
Friday, December 20, 2024
I’ve written a bit about how I deliver software, and now I want to share some of the details around how I build the browser side of web applications when I’m in charge of the project.
Language: TypeScript
My main tool is TypeScript, which I use whenever it’s feasible. I think that writing your projects in TS is the obvious best decision for the vast majority of webapps for several reasons:
- Just use one language. TypeScript and npm packages work great on both frontend and backend.
- Great frontend/backend interop story. Frameworks like RedwoodJS provide type safety across your frontend clients and backend APIs, and they make it easy to define templates to be rendered at various points in your app lifecycle: pre-rendered static bits, dynamic on content load, etc. Full-stack TS frameworks do a much better job of updating partially dynamic content in statically rendered HTML without requiring the entire app to be a big blob of single-page JavaScript application.
- Massive ecosystem. The new data format you’re trying to work with probably has an npm package. If you’re solving something that three or more developers have ever had to solve – structured logging, ANSI terminal colors, authentication, data validation – npm almost certainly has a package for you.
TypeScript does have a couple of drawbacks which occasionally make Go the better language to solve some problems:
- Poor compute concurrency. Node.js runs CPU-intensive tasks in a single-threaded event loop. If you are doing a lot of parallel computation or managing big stacks of deferred tasks, Goroutines solve your problems better.
- Complex runtime. Setting up a TypeScript project is non-trivial: you need
to configure tsc, set up
package.json
scripts, decide between ESModules or CommonJS, set up linting rules and formatters… and then to run your project on a remote machine, you need Node.js installed. The Go language comes with those tools out of the box and compiles your program into a static binary with no dependency requirements.
Components: Astro & React
For my static sites, Astro is my framework of choice. It
comes with the
Astro component language,
as well as a bunch of other helpful components which work best in Astro. For
example, the
<Image>
and <Picture>
components
make it easy to drop your images into your repo and let the framework resize
your images to the appropriate size for your target browsers. For work that
doesn’t include a lot of frontend logic, Astro provides enough for you to build
an adequate compmonent-based architecture.
When I need interactivity, I add React to my Astro apps. I first started using React in 2013 when it was released, and shortly after that, I discovered Vue v1 which I found was more effective. Vue provided “batteries included” features like state management and styling which React wasn’t really prepared to solve yet. But as React has grown, it’s become a real competitor: its integrations with state and styling libraries are now excellent. The main reason I like modern React is its functional components with hooks. With these tools, I find it very easy to reason about the data flow through a function which returns a JSX blob which ends up in my page.
State Management: Jotai
Sometimes passing state up and down from component to parent and back gets
cluttered and unwieldy. Jotai is a minimal library which
provides atoms, singletons that encapsulate a piece of stateful data. Atoms
are defined globally and can be used throughout your app, allowing you to
teleport important data wherever it needs to appear in the DOM. It also supports
storage, so you can use the atomic
paradigm to persist data such as session tokens between page loads. Most
importantly, it works very well with React via the
useAtom
hook.
Styling: Tailwind
To make my UI usable, I use Tailwind, a CSS framework with great tooling based on utility classes. In the past, I’ve used Bulma to build my apps. It’s a great CSS framework with opinions included, but eventually I find that I want more flexibility over my design language, and Tailwind provides this for me.
Using Tailwind for your styling brings many advantages:
- Comprehensive coverage. An extensive
docs library includes every
single CSS feature, from flexbox to grid to
max-width
containers with screen-sized breakpoints. With a well-selected library of named custom colors, you may not even have to open a hex picker. - Skip writing CSS. No, really – 99% of what you need to do in your app can be done by adding classes to an HTML element. You mostly don’t need to write CSS by hand to implement a desired design.
- Editor integration and linting. VSCode can help you autocomplete valid
Tailwind classes and warn you when you’re making mistakes like overwriting
Y-axis padding values
py-3
withpy-4
. - Theming and plugins. Tailwind lets you customize the class names available
for use by configuring the theme. Your
custom names are available across your class: creating
fontFamily.funky
automatically creates thetext-funky
class, even in your IDE’s autocomplete. - Performant builds. Tailwind automatically optimizes its production build to only include the class definitions which are directly relevant to your application. If you don’t use a feature, it isn’t included in the final CSS.
It also adds a few complications:
- Component systems are required. Copying
px-2 py-3 my-4
into all of your<Button>
s is tedious at best and a source of mistakes at worst. If you’re using a system that is primarily doing server-side HTML templating and makes it hard for you to compose your app out of components, using only CSS utility classes may create more problems than it solves. - Build system integration. You need to configure Tailwind to work with your app’s build system. If you have to do this by hand, you might be in for pain. Luckily, Astro and RedwoodJS both include plugins for Tailwind support.
I'd love to hear what you think about this post. Email me or @ me on Bluesky!