Subscription branching

Author: mindplay-dkCreated Jan 3, 2025Updated Jun 29, 2026

Proposal

I'd like to propose a feature I've been experimenting with that would let us dynamically fork/branch between stores, similar to how e.g. Solid signals are able to conditionally switch between different subscription dependencies, but implemented in a declarative way.

What this means in practical terms:

javascript
const $shape = atom('circle');
const $radius = atom(4);
const $width = atom(2);
const $height = atom(3);

const $area = branch($shape, {
  circle: () => computed([$radius], (radius) => Math.PI * radius ** 2),
  rectangle: () => computed([$width, $height], (width, height) => width * height),
});

const $message = computed(
  [$shape, $area],
  (shape, area) => `Area of ${shape} is ${area}`
);

The branch function takes a "control" store - when that store is updated, it takes the value of the control store and uses that to look up a property on the object, each of which are a factory function for a store you'd like to subscribe to.

branch is just a high level version of a low level function, which I call fork - so here's the long form of the $area declaration from the example above:

javascript
const $area = fork($shape, (shape) => {
  if (shape === "circle") {
    return computed([$radius], (radius) => Math.PI * radius ** 2)
  }
  if (shape === "rectangle") {
    return computed([$width, $height], (width, height) => width * height)
  }
  throw new Error(`unsupported shape: ${shape}`);
});

This might be interesting for people who prefer a more procedural style, or for other high-level functions.

Untested, but in principle, this should work with nested branches, too, I think:

javascript
const $view = branch($page, {
  dashboard: () => branch($dashboardTab, {
    overview: () => computed([$metrics], metrics => ...),
    details: () => computed([$details], details => ...)
  }),
  settings: () => computed([$settings], settings => ...)
});

I haven't actually tried this, but maybe it gives you some of idea of the direction and possibilities.

Motivation

There was an ongoing project #188 to add run-time instrumented dependency tracking.

From my perspective, runtime tracking probably isn't the right way - I've been trying to discover a functional, declarative way to achieve something similar, but with explicit subscriptions, and as an optional (tree-shakable) feature, like everything else in the existing library.

No breaking changes with the existing ecosystem of packages, so existing projects can adopt this feature incrementally or as-needed, no pressure to refactor to an entirely new API style.

There are already several other libraries that implement Solid style signals with automatic dependency tracking, such as Legend, Preact Signals, and Solid Signals by Ryan Carniato (Solid's author) which is currently in development.

There is only one library like nanostores, as far as I'm aware - so from my perspective, if we could find a way to enable dynamic subscriptions that is more in-tune with the unique value proposition of nanostores, that might be a better way to keep it relevant and different from other libraries.

Prototype

https://github.com/nanostores/nanostores/compare/main...mindplay-dk:nanostores:branching

This is very rough and just has one test, which isn't fully correct. (because of the onMount issue that I will discuss below.)

Working example

https://stackblitz.com/edit/vitejs-vite-9vcgt7qg?file=src%2Fmain.js

This example has lots of console.log statements, so you can open the console and see everything that's happening - messages prefixed with --- are from the example, and messages with - are from the fork/branch functions.

I've had a hard time coming up with a working test for this, because I don't know the codebase all that well, and the example here reveals a bug that I'm not sure how to fix:

computed-bug

In this clip, I'm switching from "circle" to "rectangle", then quickly start clicking on the "radius" button - the radius is only a dependency for computing the area of the circle, yet the first few clicks generate the "computing circle" message in the console.

This appears to be because I'm using onMount and the unmount listener has a 1 second delay. I'm not sure how to fix it. I should probably use onStop instead? I'm unsure about this - the unsubscribe functions I need to run are generated when the store is mounted, and I might be able to hoist those variables above the onMount call and make it work, but it feels risky and fragile, since this completely detaches the setup from the teardown. I feel like there ought to be an onMount that unmounts at the right time? The delay is only really relevant for preventing flicker in frameworks, I think?

Thoughts?

What do you think? Is this worth pursuing?