Usage
Key concepts
ef.js provides two fundamental concept for state management: Reactive Value and Computation.
Reactive Value is an object that holds a value and notifies its changes to dependant Computations.
You can think this as a Subject in Observer Pattern, EventEmitter in Node.js, or EventTarget in DOM.
Computation is an object that holds a callback function and reruns it everytime dependent Reactive Values has changed. In other words, Computation subscribes to Reactive Values’ changes. You can think this as an Observer in Observer Pattern or an event listener in Node.js and DOM.
Work with Signals
Create a Reactive Value
You can create a Reactive Value via signal and derived function.
Difference between the two is you can directly change signal’s containing value while derived uses a value returned by its callback as its containing value.
derived is also a Computation and hence the derived marks every Reactive Values accessed in its callback as its dependency.
import { derived, signal } from "@pocka/ef";
// The starting dollar sign ($) in variable names is
// just a convention in my source code.
// You can freely omit and use regular variable name.
const $age = signal(18);
// The value is a result of the callback function.
const $isLegalToDrink = derived(() => $age.get() >= 20);
$age.get(); // 18
$isLegalToDrink.get(); // false
// This updates $age's value, and $age notifies its change
// to $isLegalToDrink, which reruns the callback function.
$age.set(20); // 20
$age.get(); // 18
$isLegalToDrink.get(); // true
You can also use Signal and Derived class directly.
Both signal and derived are thin wrapper around corresponding class constructor.
import { Derived, Signal } from "@pocka/ef";
const $age = new Signal(18);
const $isLegalToDrink = new Derived(() => $age.get() >= 20);
// Returned object is no different
const $foo = signal(0);
const $bar = new Signal(0);
$foo instanceof Signal; // true
$bar instanceof Signal; // true
Retrieving value from a Reactive Value
Reactive Value has three way to access its containing value.
.get(computation?).value.once()
.get(computation?) returns the containing value and associates the Reactive Value to a surrounding Computation if exists.
When the optional first parameter is specified, the Reactive Value associates itself to the specified Computation instead of a surrounding one.
.value getter is an alias for .get().
.once() just returns the containing value and does nothing else.
This is suitable for retrieving a value outside of Computations or isolated callback functions.
import { signal } from "@pocka/ef";
const $count = signal(0);
$count.get(); // 0
$count.value; // 0
$count.once(); // 0
Perform action on changes
To perform something that has side effects everytime Reactive Values changed, use effect(callback).
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(() => {
console.log($count.get());
});
// logs 0
$count.set(1);
// logs 1
If you don’t want a Reactive Value to be a dependency of the Computation, retrieve a value using .once().
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(() => {
console.log($count.once());
});
// logs 0
$count.set(1);
// nothing happens
If the callback returns a function, it’ll be invoked before every rerun and disposal.
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
const log = effect(() => {
console.log($count.get());
return () => {
console.log("cleanup");
};
});
// logs 0
$count.set(1);
// logs "cleanup"
// logs 1
log.dispose();
// logs "cleanup"
Explicitly passing dependant Computation
Normally, when .get() is called on a Reactive Value, ef.js can find a correct Computation to associate.
However, as ef.js relies on a global variable due to language and tooling limitations, this mechanism does not work in callback function.
This restriction also applies to async function (Promise), which is essentially a set of callbacks.
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(function effectCallback() {
const id = setTimeout(() => {
// At this point, the execution of `effectCallback` is already completed.
// Therefore, ef.js can't figure out which Computation to associate to.
console.log($count.get());
}, 100);
return () => {
clearTimeout(id);
};
});
setTimeout(() => {
$count.set(1);
}, 200);
// wait 100ms
// logs 0
// wait 100ms
// -- script terminates --
As a workaround, a callback function of a Computation pass the Computation itself as the first argument and you can explicitly pass it to get() method.
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect((ctx) => {
const id = setTimeout(() => {
console.log($count.get(ctx));
}, 100);
return () => {
clearTimeout(id);
};
});
setTimeout(() => {
$count.set(1);
}, 200);
// wait 100ms
// logs 0
// wait 200ms
// logs 1
// -- script terminates --
However, most of the time, you should avoid this due to increase of complexity. Eager binding of the value is recommended.
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
effect(() => {
const count = $count.get();
const id = setTimeout(() => {
console.log(count);
}, 100);
return () => {
clearTimeout(id);
};
});
setTimeout(() => {
$count.set(1);
}, 200);
// wait 100ms
// logs 0
// wait 200ms
// logs 1
Resource disposal
Signal, Derived and Effect has dispose method, which stops reactivity and make them collectable.
import { signal, effect } from "@pocka/ef";
const $count = signal(0);
const log = effect(() => {
console.log($count.get());
});
// logs 0
$count.set(1);
// logs 1
log.dispose();
$count.set(2);
// nothing happens
Async helper
Due to async (Promise) is frequently used in JavaScript ecosystem and Web API, ef.js provides a helper function named asyncDerived.
import { signal, effect, asyncDerived } from "@pocka/ef";
const $name = signal("Alice");
const $profile = asyncDerived(async function (ctx, abortSignal) {
const resp = await fetch(`/my-api/profiles/${$name.get(ctx)}`, {
signal: abortSignal,
});
if (resp.status !== 200) {
throw new Error(`Unexpected API status: ${resp.status}`);
}
return resp.json();
});
effect(() => {
if (!$profile.value.isSettled) {
console.log("fetching...");
return;
}
if ($profile.value.isRejected) {
console.error("failed to fetch", $profile.value.error);
return;
}
// `.data` is a resolved data of `resp.json()`
console.log($profile.value.data);
});
Working with DOM
Element creation
The most important function in ef.js to work with DOM is el.
It creates and returns an HTMLElement with specified tag name.
import { el } from "@pocka/ef";
const div = el("div");
// div is HTMLDivElement
You can also use svg for SVG elements and mathml for MathML elements.
import { svg, mathml } from "@pocka/ef";
const path = svg("path");
// path is SVGPathElement
const mn = mathml("mn");
// mn is MathMLElement
Element setup
Second parameter of el (also svg and mathml) is an array of element setup function.
Element setup function is a function takes an element in the first argument and do something to/with the element.
import { el } from "@pocka/ef";
el("input", [
(el) => {
// el is a current element (HTMLInputElement)
el.setAttribute("type", "checkbox");
el.checked = true;
el.addEventListener("input", (ev) => {
console.log(ev.currentTarget.checked);
});
},
]);
ef.js provides three core and two extra helper functions for this task. Those helper functions are higer-ordered functions (return element setup function).
import { attr, el, on, prop } from "@pocka/ef";
el("input", [
attr("type", "checkbox"),
prop("checked", true),
on("input", (ev) => {
console.log(ev.currentTarget.checked);
}),
]);
These helper functions accept Reactive Value and update the only necessary parts on changes.
import { attr, derived, el, on, prop, signal } from "@pocka/ef";
const $disabled = signal(false);
const $name = signal("Alice");
function noop() {}
el("button", [
attr(
"title",
derived(() => `Invite ${$name.get()}`),
),
prop("disabled", $disabled),
on(
"click",
derived(() => {
if ($disabled.get()) {
return noop;
}
return () => {
console.log($name.once());
};
}),
),
]);
$name.set("Bob");
// Update "title" attribute
$disabled.set(true);
// Update "disabled" property
// Remove "click" event listener
// Add "click" event listener
Element children
Third parameter of el (svg and mathml) function is children.
You can pass Node (e.g. Element, Text), string, null and undefined.
String value is converted to Text (text node) and null and undefined are skipped.
Reactive Value containing these value is also accepted.
import { el, signal } from "@pocka/ef";
const $span = signal(el("span"));
const $name = signal("Alice");
el(
"div",
[],
[document.createElement("i"), el("p"), $span, "Foo", $name, null, undefined],
);
DocumentFragment
fragment function creates DocumentFragment with reactivity.
Available child type is same as el.
import { el, fragment, signal } from "@pocka/ef";
const $name = signal("Alice");
const profile = fragment(["Name is ", $name]);
el("p", [], [profile]);