💎 of solid-primitives, part 3: set, map, trigger
Author: @lexlohr, developer at CrabNebula and Solid.js ecosystem team member
Solid.js is already pretty powerful, but even so, there are things it cannot do out of the box. Here’s where the community comes in and provides packages to enhance your development experience: solid-primitives.
As the author of a few of those packages, I want to delve into our collection to present you a few gems that might end up being helpful to you. Here’s the third one:
@solid-primitives/set
/ @solid-primitives/map
The reactive system of Solid is undisputedly its strongest point. Its simplicity allows you to shape complex reactivity with ease. Which makes it even more surprising that Set
and Map
are not supported out of the box by Solid's stores.
Fret not, our community got you covered with the two packages mentioned above. Manipulating a ReactiveSet
or ReactiveMap
will trigger all subscribed effects.
// not reactive
const [data, setData] = createStore({ set: new Set(), map: new Map() });
data.set.add('test');
data.map.set('test', 'reactivity');
// reactive
const reactive = { set: new ReactiveSet(), map: new ReactiveMap() };
reactive.set.add('test');
reactive.map.set('test', 'reactivity');
@solid-primitives/trigger
There might be cases where neither a map nor a set will fit your use case. If you want to make your own class instances reactive, you can use the same underlying logic as the previous two primitives:
import { createTrigger } from "@solid-primitives/trigger";
class Node<T> {
#trigger = createTrigger();
#data: T | undefined = undefined;
next?: Node<T>;
push(data: T) {
this.next ? this.next.push(data) : (this.next = new Node(data));
}
pop(prev?: Node<T>): T | undefined {
if (this.next) return this.next.pop(this);
if (prev) prev.next = undefined;
return this.data;
}
constructor(data?: T) { this.#data = data; }
get data(): T | undefined {
this.#trigger[0]();
return this.#data;
}
set data(data: T | undefined) {
this.#trigger[1]();
this.#data = data;
}
}
class ReactiveList<T> {
public head = new Node<T>();
#length = 0;
constructor(init: Iterable<T>) {
for (const data of init || []) this.push(data);
}
push(data: T) {
this.head.push(data);
this.#length++
}
pop(): T | undefined {
this.#length && this.#length--;
return this.head.pop();
}
get length() { return this.#length; }
[Symbol.iterator]() {
let ref: Node<T> | undefined = this.head;
return {
next() {
ref = ref?.next;
return ref
? { value: ref.data, done: false }
: { done: true };
}
};
}
}
createTrigger()
returns a tuple of two functions, track
and dirty
. The first one subscribes effects to updates, whereas the second one will propagate updates.
In this case, having the Node handle the updates is simple, but for set and map, we cannot access the Nodes, so we have to cache the triggers based on some key. That’s where the second export of this primitive, TriggerCache
comes in. It is basically a Map of triggers.
This can be used for example to make a reactive Date object:
import { TriggerCache } from '@solid-primitives/trigger';
export class ReactiveDate {
#triggers = new TriggerCache<string>();
#date: Date;
constructor(...init: Parameters<typeof Date>) {
this.#date = init ? new Date(...init) : new Date();
}
getTime() {
triggers.forEach(this.#triggers.track);
return this.#date.getTime();
}
setTime(time: number) {
this.#date.setTime(time);
// helper to set all keys to dirty
this.#triggers.dirtyAll();
}
getSeconds() {
this.#triggers.track('second');
return this.#date.getSeconds();
}
setSeconds(secs: number) {
// this is missing a logic to handle more than 59 seconds
this.#date.setSeconds(secs);
this.#triggers.dirty('second');
}
// ...
}
Final words
We always try to provide the most utility to you, our user. If you have ideas how we could do that even better, feel free to tell us on our #solid-primitives
channel in the Solid.js Discord.
Be sure to also check the first and second installment of this series in case you missed it.