Consumption and Validation
All of Starbeam's reactivity is based on around two concepts: consumption and validation.
When you render a computation, the rendered function consumes all of the cells that were used in the computation.
If you update one of the cells used in the computation, the cell is invalidated, and any rendered functions that consumed that cell in the past are invalidated.
Crucially, it doesn't matter how the rendered function consumed the cells, and how the code used by the rendered function is structured. You use normal functions, getters and methods to build up your rendered value, and none of that code needs to be aware of reactivity at all.
Reactive Collections
Reactive collections, like Map
and Set
, work exactly the same way.
For example, if you get
a value from a reactive map in a rendered function, the rendered function consumes the cell for that map entry. If you set
the value later, that cell is invalidated.
And if a rendered function iterates over a reactive Map
, it consumes a cell for the iteration. If you later set a value, delete an entry or clear the map, the iteration cell is invalidated, which invalidates your rendered function.
Example: Reactive People List
To demonstrate this point, let's create an object that uses a reactive array under the hood, but exposes a normal JavaScript API.
tsximport { reactive } from "@starbeam/js"; interface Person { name : string; location : string;} class People { #people = reactive .array <Person >([]); push (person : Person ): void { this.#people.push (person ); } [Symbol .iterator ](): IterableIterator <Person > { return this.#people[Symbol .iterator ](); } byLocation (location : string): Person [] { return this.#people.filter ( (person ) => person .location === location , ); }} const people = new People ();
tsximport { reactive } from "@starbeam/js"; interface Person { name : string; location : string;} class People { #people = reactive .array <Person >([]); push (person : Person ): void { this.#people.push (person ); } [Symbol .iterator ](): IterableIterator <Person > { return this.#people[Symbol .iterator ](); } byLocation (location : string): Person [] { return this.#people.filter ( (person ) => person .location === location , ); }} const people = new People ();
tsximport { reactive } from "@starbeam/js"; class People { #people = reactive .array ([]); push (person ) { this.#people.push (person ); } [Symbol .iterator ]() { return this.#people[Symbol .iterator ](); } byLocation (location ) { return this.#people.filter ( (person ) => person .location === location , ); }} const people = new People ();
tsximport { reactive } from "@starbeam/js"; class People { #people = reactive .array ([]); push (person ) { this.#people.push (person ); } [Symbol .iterator ]() { return this.#people[Symbol .iterator ](); } byLocation (location ) { return this.#people.filter ( (person ) => person .location === location , ); }} const people = new People ();
We want to render a comma-separated list of people from New York, using people.byLocation("New York")
.
tsximport { DEBUG_RENDERER } from "@starbeam/universal"; DEBUG_RENDERER .render ({ render : () => people .byLocation ("New York"), debug : (people ) => { console .info ( people .map ((person ) => person .name ).join (", "), ); },});
tsximport { DEBUG_RENDERER } from "@starbeam/universal"; DEBUG_RENDERER .render ({ render : () => people .byLocation ("New York"), debug : (people ) => { console .info ( people .map ((person ) => person .name ).join (", "), ); },});
How does byLocation
consume reactive cells?
- It accessed the reactive
#people
array, stored in a private field - It used
Array
'sfilter
method to iterate the array
Next, we'll add some people to the array.
tsxpeople .push ({ name : "John", location : "New York" });people .push ({ name : "Jane", location : "New York" });people .push ({ name : "Joe", location : "London" });
tsxpeople .push ({ name : "John", location : "New York" });people .push ({ name : "Jane", location : "New York" });people .push ({ name : "Joe", location : "London" });
And how did this code update reactive cells?
- It calls
push
on thePeople
class - The
push
method accessed the reactive#people
array - It used
Array
'spush
method to add items to the array
This invalidated the array's iteration, which invalidated the rendered function.
Finally, since invalidation simply schedules revalidation, our renderer only ran once.
Rendering With the Debug Renderer
There's no need to do any kind of additional batching, debouncing or scheduling, since no values are pushed through the system that need to be intercepted and massaged.
In fact, if the rendered function was removed before the revalidation occurred, nothing at all would happen! Again, that's because the invalidation simply scheduled the renderer to revalidate, and by the time it was ready to revalidate, it didn't exist anymore.
This may seem like a subtle point, but it's very important. It's what makes it possible to use normal tools of JavaScript composition and abstraction to build reactive systems without even thinking about reactivity, and still have them behave correctly.