ef.js

Declarative DOM helper experiment


status: accepted

Use Signals for reactive API

Context and Problem Statement

Development of ef.js has started as Signals + DOM library. However, I noticed Signals does not work well outside of synchronous function call stack, during implementing the library.

Are there any tricks covers this drawback? Can other API design style work well with el function?

Decision Drivers

Considered Options

Decision Outcome

Chosen option “Signals”, because of the simplicity and ease of use. This option also is least error-prone compared to the other two.

Pros and Cons of the Options

Signals

The original plan. API design pattern implemented in S.js, Solid.js, Preact, etc.

// Create a reactive value.
const $age = signal(10);

// Create a derived reactive value, which is read-only.
const $isLegalToDrink = derived(() => {
  // NOTE: This is the drinking age in Japan.
  return $age.get() >= 20;
});

// Runs a function everytime the reactive values inside the function got changed.
effect(() => {
  console.log($isLegalToDrink.get());
});

el("div", [attr("data-legal", $isLegalToDrink)]);

setTimeout(() => {
  $age.set(21);
}, 1_000);

Observer pattern

Simple Observer class and Derived class (equivalent for derived function).

const $age = new Observable(10);

// `as const` is required
const $isLegalToDrink = new Derived([$age] as const, (age) => {
  return age >= 20;
});

$isLegalToDrink.subscribe((isLegalToDrink) => {
  console.log(isLegalToDrink);
});

el("div", [attr("data-legal", $isLegalToDrink)]);

setTimeout(() => {
  $age.write(21);
}, 1_000);

Signals-like API with manual scope management

Eliminating the need of mutable global variable by exposing the internal computation scope.

const { effect, signal, derived, write } = scope();

const $age = signal(10);

const $isLegalToDrink = derived(({ read }) => {
  return read($age) >= 20;
});

effect(({ read }) => {
  console.log(read($isLegalToDrink));
});

el("div", [attr("data-legal", $isLegalToDrink)]);

setTimeout(() => {
  write($age, 21);
}, 1_000);