💎 of solid-primitives, part 3: set, map, trigger

CrabNebulaCrabNebula
3 min read

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.

0
Subscribe to my newsletter

Read articles from CrabNebula directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

CrabNebula
CrabNebula

CrabNebula is built on the vision of enabling individuals, entrepreneurs and businesses to sustainably build, develop and distribute their apps to the universe.