Weeknotes 2024 W50: Lense signals

December 9​–​15, 2024
1400 words

Quick bits:


Shower thoughts:


I’ve been torn by how I want to proceed with my word processor. In the mean time, I whipped up something with Svelte and Electron:

A screenshot of a word processor. It looks a bit like Scrivener.

I’m impressed by how far I’ve managed to get in my spare time in a week:

A lot of this is handled by Electron, which is something I barely had any experience with.2 I looked into Tauri at Renaud’s suggestion, but it doesn’t have quite the featureset I’m looking for, especially around document handling (managing recent documents, and setting the document represented by a window). Wails is in the same boat. And so, Electron it is.

There are things I miss from SwiftUI/AppKit. A GUI that looks good by default and is built with accessibility in mind. An outline view (tree view) with a proper drag-and-drop3 implementation. Runtime performance, too, of course.

But there is a lot to like about developing GUI applications with JavaScript/Type­Script. Development is much swifter4 than with SwiftUI, with near-instantaneous live-reloading. The fact that I whipped up this application in a few hours of work is testament to the development speed. Building this in JavaScript also opens up the possibility of having a cross-platform app, or a hosted web app, perhaps with real-time collaboration.

It’s not just JavaScript, either: CSS’ flexbox is amazing for building GUIs. That toolbar was a cinch to build with CSS.

But JavaScript development is not without its problems. I used Svelte for this prototype, and ran into a problem where parts of the application would no longer re-render when they should. Svelte uses proxies to handle reactivity (which it funnily calls deep state) but in my case, some objects stopped being proxies. I could not for the life of me figure out why.

State management is tough in general, but it is even more so for this application. Even for this prototype, the data model is far from trivial: some objects have attributes (like label) that reference user-definable objects. Some state changes need to mark the document as having changed (“dirty”) while others do not. But most annoyingly, objects can be nested arbitrarily deep, and some parts of the GUI need that tree of objects as a flat list instead, and that flat list needs to be kept in sync with the hierarchical list at all times. Ugh.

Svelte all but falls apart with a data model this complex. I need something more debuggable and with much less “magic.” I feel like I’m still in the process of eliminating potential solutions. Two weeks ago I was creating the prototype with Solid, and I frankly have no idea why I abandoned it — Solid seemed just fine.

I’d say “ugh, so much work” but I haven’t really spent all that much time on it. I am speedrunning this whole thing.


I’m playing around with the combination of signals with my Lenses approach. It’s rather neat: the top and setTop pair in my forms example maps directly onto signals. Here it is for useState:

interface BareTextFieldProps<T> {
  lens: Lens<T, string>;
  top: T;
  setTop: (t: T) => void;
}

And here it is for signals:

interface BareTextFieldProps<T> {
  lens: Lens<T, string>;
  signal: Signal<T>;
}

With useState, you’d pass in both top and setTop, like this:

  <BareTextField
    top={person}
    setTop={setPerson}
    lens={firstNameLens}
  />

With signals, you can instead pass just the signal:

  <BareTextField
    signal={person}
    lens={firstNameLens}
  />

Getting and setting values with useState is like this:

  let value = lens.get(top);

  let setValue = (newValue: string) => {
    setTop(lens.set(top, newValue));
  };

With lenses, it’s similar:

  let value = lens.get(signal.value);

  let setValue = (newValue: string) => {
    signal.value = lens.set(signal.value, newValue);
  };

Signals and lenses work quite well together. How neat is that?!

It might even be worth creating a type for a signal as seen through a lens: a Signal<T> seen through a Lens<T, F> could be a LensedSignal<F> which would behave similar to a signal itself — not dissimilar from derived state. More experimentation and thinking is needed here, though.

Still, the value of lenses is diminished a little when using signals, because you can nest signals and pass around the nested signal instead. Nonetheless, lenses remain useful, because you wouldn’t create nested signals for every nested bit of data.5

Perhaps I need to rewrite my prototype to use signals and lenses. I’m only half joking: I am at a hilarious number of rewrites, but at least the signals-plus-lenses approach would make state management much more explicit, and thus much easier to reason about, and thus much easier to debug.


Entertainment:


Links:

Entertainment links:

Tech links:


  1. A tumor! Technically. But doctors don’t use that word because it has many irrelevant and wrong connotations. You had a different idea of what it was before you read this sidenote, did you not? ↩︎

  2. I have touched the Pitch Electron app when I was working there, but I never really got to know it super well. ↩︎

  3. I’ve learnt a great deal about implementing drag and drop in HTML/JavaScript. It’s not terribly difficult, but it is something I would have loved to avoid, especially for outline views. ↩︎

  4. Pun entirely intended. ↩︎

  5. Nested signals are neat, but the practices around them are hardly documented. Maybe I’ll do a write-up on them some time. ↩︎

  6. Carol, directed by Todd Haynes, written by Phyllis Nagy and Patricia Highsmith (The Weinstein Company, Film4, Number 9 Films, 2016). ↩︎

  7. When is a movie a Christmas movie? I don’t know. I also don’t care. A movie is a Christmas movie when you want it to be one. We are not Christmas movie prescriptivists in this house. This is a hill I will die hard on. ↩︎

  8. ELEX (Piranha Bytes, 2017), published by THQ Nordic. ↩︎

You can reply to this weeknotes entry by email. I’d love to hear your thoughts!
If you like what I write, stick your email address below and subscribe. I send out my weeknotes every Sunday morning. Alternatively, subscribe to the web feed.