Dependency-tracking reactivity

What if the language had dependency-tracking reactivity?

For example, with some contrived syntax, in the following example, any time foo or bar change, both values are logged to console:

reactive foo = 0
reactive bar = 0

autorun {
  console.log(foo, bar)
}

setInterval(() => foo++, 1000)
setInterval(() => bar++, 820)

The autorun expression reruns any time any of the reactive variables (auto-detected dependencies of the autorun) change. By using the variables inside the autorun, the system automatically tracks those variables as dependencies of the autorun, and that's how it knows to automatically rerun the expression if any of the dependencies change.

For those not familiar with dependency-tracking reactivity, it is a key feature in these libs or frameworks (note that API naming varies across them):

and many more.

If we want to stop logging in the above example, we could perhaps write

reactive foo = 0
reactive bar = 0

const stop = autorun {
  console.log(foo, bar)
}

setInterval(() => foo++, 1000)
setInterval(() => bar++, 820)

// Stop the autorun (stop logging) later:
setTimeout(stop, 10_000)

I also wrote about how dependency-tracking reactivity makes code shorter, cleaner, and more concise compared to other event patterns here:

Do you think we can have a language-level dependency-tracking reactivity feature built in?

some related threads:

Actors Beat you to it, just in a slightly different flavor. :wink:

I think Actors is on a better path than most frameworks using today's JS syntax except for the ones with dependency-tracking reactivity and fine-grained updates. I replied there on how I think syntax could be, and to me it feels more natural that way.

Are you familiar with Qt? Dependency-tracking reactivity is built into Qt's QML+JavaScript language.

Many of us in the Solid.js community have been thinking about this. I think that for this to work, it needs unique syntax so that it is essentially new primitives that don't conflate with existing paradigms in the language, like I described in a comment to Ryan's article:

With syntax being bike sheddable, that comment, plus this snippet gets the idea across:

export signal count@ = 0

// increment it every second
setInterval(() => count@++, 1000)
import {count@} from './count.js'
import {someOtherValue@} from './somewhere.js'

const effect = effect {
  // This runs any time either count@ or someOtherValue@ change, batch-deferred in the next microtask after either was modified
  console.log('values are: ', count@, someOtherValue@) 
}

// any time later,
effect.stop()

Sometimes we want read-only signals:

signal _count@ = 0

@readonly // perhaps using a future decorators proposal, decorating the export
export signal count@ = _count@

// increment it every second
setInterval(() => _count@++, 1000)

Or maybe we hav enew syntax for readonly derived values:

signal _count@ = 0

export memo count@ {
  return _count@
}

or

signal _count@ = 0

export memo count@ = _count@

Just food for thought, but the main idea is syntax differentiates the semantics from current JS features. The meaning of existing languages features is not ambiguous this way. A regular var foo will always operate like it currently does, never being a reactive signal (unless future declaration decorators are added, which allow people to achieve it via decorating get/set of a variable).

There would be a new "pass by signal" that passes the signal along, so that it can be reacted to anywhere. F.e.:

signal foo@ = 123

setInterval(() => foo@++, 1000)

function makeAnEffect(bar@) {
  effect {
    console.log(@bar)
  }
}

var someVar = 456

makeAnEffect(123) // syntax error, 123 is not a signal, it cannot be passed to a signal parameter.
makeAnEffect(someVar) // syntax error, someVar is not a signal, it cannot be passed to a signal parameter.

makeAnEffect(foo@) // this works, foo@ is a signal, and the internal bar@ reads from it (merely an alternate name for the same signal within the function.

Without the syntax disambiguation, things will get very messy very quickly.

1 Like

You'd probably want a cleanup mechanism to go along with this (thinking of Surplus' S.cleanup or Solid's onCleanup hook).