Tech Stack Overview: Homechart's TypeScript/Mithril Frontend

August 22, 2022 Mike

In part one of Homechart’s tech stack overview, we explored Homechart’s HTTP API server component. In part two, we’ll explore Homechart’s frontend architecture.

This component provides an interactive, easy to use user interface for performing actions against the HTTP API server component. By leveraging web technologies and packaging tools, we can deliver the same user interface on the web, desktop, Android, and iOS with ease as a Progressive Web Application (PWA).

Codebase Layout

The Homechart frontend component is a collection of HTML, JavaScript, and CSS:

  • HTML describes the layout/content of the frontend
  • CSS describes how the HTML should look
  • JavaScript makes the HTML interactive

Instead of writing all of this code independently, we leverage a JavaScript framework called Mithril to describe HTML/CSS/JavaScript at the same time. And instead of writing all of this in JavaScript and introducing bugs/complexity as we scale our application, we use an extension called TypeScript to ensure our code is consistent and correct.

Yes, compared to the backend, the Homechart frontend has quite a bit of needless complexity related to the underlying technologies. And unlike Go, there aren’t a lot of guidelines for frontend codebases, especially with a more “freeform” framework like Mithril.

Frontend codebase root

Below, we’ll review the following sections of the codebase:

Tooling

All of the .json and .js files in the root directory are configuration files for various JavaScript tooling. Here are the tools Homechart uses as part of its build process:

  • ESLint - Reviews our JavaScript/TypeScript code for correctness.
  • Stylelint - Reviews our CSS code for correctness.
  • Vite - Takes our code and outputs the UI assets for the Homechart API server and app stores.

Android

This folder contains code for the Homechart android app. The code is a wrapper around the Homechart frontend created by bubblewrap, which creates a Trusted Web Activity (TWA) from our PWA. Basically it’s a complicated process to get the Homechart app on the Google Play Store.

iOS

Unlike Google, Apple has stricter guidelines around apps and hasn’t fully embraced all of the web technologies for PWAs/TWAs. Instead of rewriting Homechart for iOS, we use another wrapper around the frontend code called Capacitor. The end result is quite similar to the TWA setup–another complicated process to get Homechart on the Apple App Store.

src

This folder contains the actual frontend codebase:

Frontend codebase

Here are the parts we’ll review:

Root files

Our build tool needs an “entrypoint” into the codebase: index.html and index.ts. index.ts combines the Mithril routes (/settings/account, /signin, /calendar, etc) defined in routes.ts and initializes the state directory.

Mapping routes to views in routes.ts with code splitting

Homechart leverages a Service Worker to handle background web tasks like Push Notifications and caching assets so Homechart can work offline.

components

Wherever possible, we try to reuse our code via Mithril components. Combined with TypeScript, they let us effortlessly reuse parts of our UI like a Button or a Table by exposing properties other components can set to change their behavior.

An example component
Using the component

css

We embed most of our CSS definitions within each component (or rarely, a view). This lets us keep our CSS definitions tightly scoped and easily changeable.

Component CSS

To keep the styles consistent across components, we leverage CSS variables. These and some other “global” CSS styles live in the css folder.

Global CSS variables

CSS variables are extremely handy. Instead of having to declare all of the various media queries in each component, we declare them in the main variables.css file and use variables to change things:

Centralized CSS media query values

layout

This folder contains the main layout definitions for Homechart–App, AppMenu, AppHeader, AppFooter, etc. All pages are “wrapped” with this layout, keeping the framing of the app consistent between views.

Example layout code

services

This folder centralizes various services Homechart uses:

  • http requests via fetch
  • data storage via IndexedDB
  • logging to the console and tracing
IndexedDB service

states

One benefit of using Mithril is we aren’t bound by any specific state paradigm, so we can implement one that is tailor made for our application. Our current state setup involves having a class for each object type in Homechart (BudgetAccount, PlanTask, etc) that inherits from a base class (avoid duplication) and leverages Mithril Streams to make the data reactive.

storeNames will automatically update when data changes

The end result is a set of independent state files that can be updated by the user directly or in the background via SSE.

SSE handling

With all the data changes happening, we need a way to offload the operations so we don’t hang the UI. We leverage Web Workers for this.

views

Routes map URLs (/settings/account) to views which combine components and state into screens for the user to interact with. With the help of component reusability, the views end up being fairly small and easy to read.

An example view

workers

Since JavaScript is single threaded, all actions are queued and acted upon sequentially. With an interactive UI, a long running task could hang the interface as any user actions become queued and delayed.

Web Workers are a way to perform things on a separate thread/in the background without impacting the UI. Due to the way data is passed between the main thread and a web worker, a lot of actions become difficult or costly to implement in a web worker paradigm, make sure you research/validate before you go down this path!

Homechart leverages Web Workers to perform specific actions around data operations and fetching data.

The Web Worker handler

Instead of spinning up web workers on demand, Homechart leverages a set of long running Web Workers in a pool and submits tasks to them.

The workers inspect the task to determine what type of task it is, perform the task, and then send the result back to another handler in Homechart which figures out what to do with the result. This has been a good tradeoff for us in terms of complexity and performance, you may end up with a different solution.

Summary

The Homechart frontend is quite a bit more complicated than the API due to the nature of the ecosystem. Web technologies and JavaScript are constantly changing, and your codebase needs be capable of evolving with them.

We chose to use Mithril at the creation of Homechart due to the flexibility it provides, and have stuck with it even in the face of newer, more exciting frameworks like React, Vue, and Svelte for the same reason. For us, Mithril gives us just enough functionality without adding mental load to understand what it’s doing. We also strive to limit our dependencies and keep our code as simple as possible.

We hope you enjoyed this overview of Homechart’s frontend. In part three, we’ll checkout our tooling and CI/CD processes.