Reactivity

Knowing how state works in Torpor is enough to get you up and running, but it may also be helpful to know a bit more about how reactivity works in more depth.

The reactivity system is a pull-push-pull signaling system, similar to the systems used in other frameworks like Solid, Preact, Svelte and Vue. It was largely inspired by the Preact signals implementation.

In a reactivity system like this, the rough idea is that:

  1. Effect functions are run
  2. The effect functions pull values from signal containers
  3. The signal containers push their values to the effect functions when changed
  4. The effect functions re-run and pull new values from the signal objects

The whole process is repeated many times, for as long as your components are in use.

Signals and effects

In Torpor, signals are created by using the $watch function to convert an object into a reactive Proxy object that we use to intercept property accesses:

let $state = $watch({
	count: 1
})

Each property accessor is essentially a signal that re-runs dependent effects when changed.

Effects are created by using the $run function:

$run(() => {
	console.log($state.count);
})

When the property of a reactive object is accessed, a subscription from the property to the effect is created. When that property is subsequently updated, all of the effects that have subscribed to it will be re-run. In the course of that re-run, the subscription may be re-used, it may be disposed if no longer required, and new subscriptions may be created. All of this happens behind the scenes to keep your state and UI in sync.

Caching

You can use property getters to return a calculated value:

let $state = $watch({
	count: 1,
	get isPrime() {
		if (this.count < 1) return false;
		for (let i = 2; i < this.count; i++) {
			if (this.count % i === 0) return false;
		}
		return true;
	}
})

However, if you are accessing this property's value in multiple effects, it will be run over and over again, slowing down your application. To ensure that it's only run when its dependencies change, you can wrap the getter's implementation in a $cache function:

let $state = $watch({
	count: 1,
	get isPrime() {
		return $cache(() => {
			if (this.count < 1) return false;
			for (let i = 2; i < this.count; i++) {
				if (this.count % i === 0) return false;
			}
			return true;
		});
	}
})

Conceptually, cached signals sit somewhere between signals and effects, both pushing (when their value changes) and pulling (when they need to be re-run). Because of this, they complicate the dependency graph and use up memory, so you should only use them when you need to.

In other frameworks cached signals may be called computed, derived, or memo(ized).

Playground

Input

Output