npm i vellyrThis is a small signal library with fairly minimal API surface. The signals are not lazy, so all derived signals will be automatically re-computed on any sources change.
To mitigate that, you can pass a custom comparator when creating/deriving a new signal. Here is an example of how you can use it:
import { vellyr } from "vellyr";
const state$ = vellyr({
items: [
{ active: true, name: "first" },
{ active: false, name: "second" },
],
});
const filteredIds$ = state$.map((state) =>
state.items
.filter((item) => item.active)
.map((item) => item.id),
{ equality: shallowEqual }
);
filteredIds$.on(ids => {
console.log(`active ids: ${ids}`)
})vellyr(initialValue)vellyr(initialValue, { equality: (value1, value2) => value1.id === value2.id })vellyr.only(value)vellyr(vellyr.empty)
By default, you probably want to provide the first value, and you can also provide the equality function. You can create a signal which cannot change its value (it simply does not have set() or update() calls), but can be used to derive values. Finally, you can manually provide an empty value, which will stop derived signals like .combine() from being triggered before you set the first value.
Signals provide straightforward imperative API to interact with the current value and to update it.
signal$.get()signal$.getError()(more on the errors later)signal$.set(value)signal$.update(currentValue => currentValue + 1)
The power of signals (or any observable primitive for that matter) comes from their composable nature. This library aims for a very small set of available methods to derive new signals from existing ones, but it should be enough for common use-cases and extending them.
signal$.map(value => value * 2)signal$.map(item => sanitizeItem(item), { equality: equalityFn })
Map allows you to transform an existing signal into something else, which is very useful. You can provide a custom equality function, and the signal you are deriving from can have its own equality function as well.
clicks$.combine(item$, request$).map(([click, item, request]) => ...)
Combining multiple signals simply creates a signal with each value as a tuple. You cannot provide a custom equality function to this method (if you really need one, you can map the result with the identity and provide an equality function at this point).
values$.filter(value => value > 5)
Signal which will only receive a new value if the filtered function returns true. You can provide the equality function here as well.
const sum$ = values$.scan((acc, value) => acc + value, 0)
To achieve reduce functionality, use .scan() to store the accumulated value. You can also provide the custom equality function as the last argument.
To actually listen to value changes, there is .on() method, which comes in 2 flavours:
const unsub = value$.on((value) => { ... })const unsub = value$.on({ next: ( value) => { ... }, error: e => { ... } })
They are equivalent, but the second form is the only way to subscribe for errors. The errors are propagated down, so an error on a parent signal will propagate down the children.
To clean up parent/children dependencies correctly, you need to call signal$.dispose() when the signal is not relevant anymore. This is needed because parents/children need special tracking internally so that combining multiple signals with the same parent will only be updated once.