Stateful Reactivity with Preact Signals

Preact signals is a small reactive programming library for building user interfaces. It allows you to describe the data dependencies between signals, which the reactive engine will then keep in sync.

const x = signal(1);
const doubled = computed(() => x.value * 2);

console.assert(x.value === 1);
console.assert(doubled.value === 2);

x.value = 100;
console.assert(doubled.value === 200);

A common pattern in data visualizations is to use dynamic scales, which mediate between the data domain and the visual domain. For example, a linear scale might be used to determine bar widths in a bar chart.

Bar chart

This scale used for the bar width needs to be dynamic, since when width of the page changes, the width of the bars should change with it.

Implementing this pattern with signals turned out to be less straightforward than I expected due to the caching behavior of the computed primitive. As a performance optimization, a computed signal is only be considered to have changed when its new value is not === to its prior value, which means that using a computed signal to mutate and return the scale object can't work, since the change won't propagate to any dependents of the scale signal.

I wrote a small helper function, recomputed, which acts like an un-cached computed and propagates its value whenever its dependencies change, regardless of whether the value is the same:

function recomputed(reduce, initialValue) {
  let value = initialValue;
  const changed = computed(() => {
    value = reduce(value);
    return NaN;
  });
  return {
    get value() {
      changed.value;
      return value;
    }
  };
}

This lets us implement a “reducer” pattern where a stateful object like a d3 scale is updated in response to changes in its dependent signals.

For example, here’s how we can use recomputed to implement a reactive linear scale that depends on a width signal:

const width = signal(800);
const scale = recomputed(
  (s) => s.domain([0, width.value]), 
  d3.scaleLinear()
);