DEV Community

Alex Lohr
Alex Lohr

Posted on

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

Author: @lexlohr, developer at @crabnebuladev 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');
Enter fullscreen mode Exit fullscreen mode

@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 };
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
youngfra profile image
Fraser Young

How does TriggerCache compare to using a regular reactive store in terms of performance for handling more complex reactive objects like ReactiveDate? Would love to see a future post diving deeper into use cases for createTrigger()!