This is a great minimal start (love the zero deps).
However, to be truly useful for UI patterns, we need derived state. If I have width and height signals, I should not have to manually subscribe to both to update area.
I would love to see a computed(() => ...) primitive that tracks dependencies automatically. Are you planning to add dependency tracking (e.g. global context stack) or keep it purely explicit?
4 Comments
Magic is bad. Global state stacks are fragile.
If you need derived state, pass the dependencies explicitly.
computed([width, height], (w, h) => w * h)
Clearer to read, easier to debug, no global context needed.
I lean towards explicit dependencies too, simply for predictability.
However, if we are building a "Signal" library, people usually expect that reactive magic.
Maybe a compromise? computed(deps, fn) where deps is an array of signals. It is verbose but strictly typed and you can see exactly what triggers the re-calc. Implicit dependency tracking via global stacks is where the bugs hide.
Explicit deps array is acceptable. It forces the dev to think about the graph.
If deps matches fn arguments, even better.
computed([a, b], (a, b) => ...)
Prevents the "magic global" problem where you accidentally depend on something just by reading it.
Explicit dependencies feel safe, but they re-introduce the stale closure and forgotten dependency bugs we fought in React hooks for years.
A well-implemented context stack isn't fragile—it's how every modern signal library works (Solid, Preact, Vue). The DX win of const area = computed(() => width() * height()) is massive compared to maintaining a dependency list manually.
The code should describe the relationship, not the mechanics.