Help! None of my projects want to be SPAs

My strategy for dealing with the absurd pace of change in web development has been as follows: ignore 99% of it and see if it goes away.

Given the hype cycle, it works pretty well. Mongo isn’t exciting anymore, Angular 1 is dead, CoffeeScript is obsolete, I haven’t heard a word about Meteor since it launched…

It beats being a Magpie Developer. I have more time time to pet my dog, read a lot of books, make fresh pasta by hand, and circle a more slowly evolving collection of powerful technologies that let’s me get stuff done without spending half my free time chasing shiny new things.

More Tagliatelle, less code.

By and large I find enough time to explore new browser features like CSS Grid and Service Workers—things I can actually use.

1.

This brings us to React.

Several years into its hype cycle, it’s probably safe to say the React patterns, if not the library itself, has staying power.

I did the Wes Bos tutorial a few years ago, said, yep, this is cool, and it would be a good tool for the job if you made an app with a lot of changing visual state, like Asana or Jira. But a news site, for example, which is all about reading text on a page, doesn’t reap the benefits.

Since then, I’ve also noticed another possible benefit: if you have a big team with a range of skill levels, it gives you a predefined pattern on how to break up and assign work without every person needing to understand the whole organism.

That’s all well and good. One of these days, I thought, I should do a toy project in it. And after a year of looking for one, I’m starting to realize something very odd: I can’t find one.

2.

I don’t make side projects for other people anymore, but I make all sorts of little things for myself. In addition to this site, I have a homemade expense tracking app an automated newsletter to myself that’s composed from RSS feeds… etc. I’ve toyed with a cooking app a few times but concluded without a lot of manual work to turn recipes into structured data, it’s not actually better than Dropbox Paper.

Looking at these, they’re almost all CRUD apps, even if the “Create” comes from an API, RSS feed, or webhook. Something puts data in them, and they do something with the information that you couldn’t do 10 years ago when it was tied up in Email/Documents/Spreadsheets.

And one more thing: it’s possible to make fast, efficient CRUD apps the same way we made them in 2010. Fire up Django or Rails, stick the data in Postgres, put some thought into your forms and maybe add a tiny bit of javascript to enhance some input fields.

Surely I must be missing something. Surely the billions of programming hours that have gone into all these new things aren’t in vain.

3.

But never mind that. I decided to build my own Pinboard replacement. Pinboard is… pretty good, but I like the idea of a hybrid between a bookmarking service that could also keep images and markdown notes.

So I started playing around, using Django Rest Framework for the API, Rollup to build because Rollup is easy, Preact because its smaller and faster.

I’m liking classes for bigger components like the bookmarks index and stateless functions for each item in the list. There’s a natural obviousness to writing handleClick methods for everything that can be clicked on.

I’m using a pubSub pattern for state management because Redux, while people like it, felt wildly overcomplicated for what I wanted to do. The components listen on a single shared pubSub event bus, but can tell any API service to update itself, which will then kick off the event.

Reduced to an illustrative example, it looks something like this.

import { pubSub, EVENTS, bookmarksService } from "../services"; import { SearchForm, BookmarkComponent } from "./other-components"; export class BookmarkIndex extends Component { handleLoadBookmarks({ bookmarks, filters, pagination }) { this.setState({ marks: bookmarks, filters: filters, pagination: pagination, }); } componentWillMount() { pubSub.on(EVENTS.load_bookmarks, this.handleLoadBookmarks.bind(this)); bookmarksService.update(); } componentWillUnMount() { pubSub.off(EVENTS.load_bookmarks, this.handleLoadBookmarks.bind(this)); } search(e) { e.preventDefault(); const query = e.target.querySelector('[name="search"]').value; bookmarksService.update({ search: query }); } handleClear(e) { e.preventDefault(); bookmarksService.update({}); } render(props, state) { const marks = state.marks || []; return ( <div class="l-app"> <div class="l-main"> <SearchForm searchAction={this.search.bind(this)} /> <section class="c-marks"> {marks.map(BookmarkComponent)} </section> </div> </div> ); } }

That’s kind of cool. I’ve got a clear, predictable one-way data flow without any of the cognitive overhead I saw in the popular state managers. A service represents shared state. Any component ask the service to do something, and any component can listen for an update.

And yet, I just spent an enormous amount of time implementing what would have been something like this if it were just server-side Python[^3]:

def bookmarks(request): bookmarks = Bookmark.objects.filter(user=request.user) search = request.GET.get('search', None) tag = request.GET.get('tag', None) if search: bookmarks = bookmarks.special_search_method(search) if tag: bookmarks = bookmarks.search_by_tag(tags=tag) return render(request, "bookmarks.html", { "bookmarks": bookmarks, })

The Preact stuff is kind of fun. But it feels like adding a lot of complexity, and I can’t pin down how it benefits most of these projects.

4.

Back in the early days of SPAs, some people argued that it would be faster to only pass the data you need as JSON than to render whole pages.

Nearly a decade later, this is almost never true.

The “Recent Charges” page of my budget app is 5kb of HTML.

The Pinboard clone loads 400 bytes of HTML, followed by 36kb of javascript, and 35kb of data from the API. The JS bundle alone is more than 7x the weight of the rendered page, can’t be called until the first HTTP request completes, and still has to make an API call.

In theory subsequent actions might be faster once the page is loaded. That may be true, particularly if the pages are cluttered, and only some components need to update on an action.

But that assumes you make subsequent actions more often that initial actions. That may be true for Slack. But if I think about how I use Google Docs, Dropbox Paper, Atlassian apps, or almost any other large js-heavy application, I find myself jumping in and out of them alongside other things.

That’s especially true for a bookmarks manager: my main use case is to open in and search for something very specific.

5.

I can think of a few things that would benefit from being SPAs.

A chat application is a no-brainer. A dashboard of realtime data might not need to be a SPA, but would benefit from a reactive component that updates when the API is polled. Maybe a complicated tool like Trello, a Calendar or a Nonlinear Video Editor could make sense, but even those have distinct screens, so a large reactive component on a particular screen may be a better fit.

What’s more interesting is how many apps I’m seeing built as an SPA that really don’t need to be. Why does a website that orders food from restaurants need a Megabyte of javascript?

I tried to figure that out by inspecting the API calls. It turned out they were tracking every mouse event. So that’s nice…

6.

So why consider this app in Preact in the first place?

It’s because of Progressive Web Apps.

I’d like to try and make it work offline. To do that, and do it well, you need a way to show the data in different ways even when you can’t call a server. This calls for javascript templating.

And so it goes.

At this point I finished the list page—it has pagination, search, tags, etc. all working—but still need to do the create and edit. It’s taken longer than I feel like it should, and I haven’t even started the Service Worker component.

7.

I don’t have a conclusion. It’s messy and complicated. As a way to learn, the Bookmarks project is fun. As a productive and maintainable thing, it’s not even close.

I may tear it down the moment I finish and switch to a traditional MVC style, or find a more minimalistic approach that can work well with offline support.

In the meantime, stop coding and pet your dog. He would appreciate a belly rub.