universal-stores
State management made simple.
Stores are a simple yet powerful way to manage an application
state. Some examples of stores can be found in Svelte (e.g. writable, readable) and Solid.js (e.g. createSignal).
This package provides a framework-agnostic implementation of this concept.
NPM Package
npm install universal-stores
Documentation
Store
A Store<T>
is an object that provides the following methods:
subscribe(subscriber)
, to attach subscribers;set(value)
, to update the current value of the store and send it to all subscribers;update(updater)
, to update the value using a function that takes the current one as an argument.
There is also a getter value
that retrieves the current value of the store:
import {makeStore} from 'universal-stores';
const store$ = makeStore(0);
console.log(store$.value);
store$.set(1);
console.log(store$.value);
When a subscriber is attached to a store it immediately receives the current value.
Every time the value of the store changes (by using set
or update
) all subscribers get the new value.
Store<T>
also contains a getter (nOfSubscriptions
) that lets you know how many subscriptions
are active at a given moment (this could be useful if you are trying to optimize your code).
Let's see an example:
import {makeStore} from 'universal-stores';
const store$ = makeStore(0);
console.log(store$.value);
const unsubscribe = store$.subscribe((v) => console.log(v));
store$.set(1);
unsubscribe();
store$.set(2);
store$.subscribe((v) => console.log(v));
Let's see an example that uses the update method:
import {makeStore} from 'universal-stores';
const store$ = makeStore(0);
store$.subscribe((v) => console.log(v));
const plusOne = (n: number) => n + 1;
store$.update(plusOne);
store$.update(plusOne);
store$.update(plusOne);
A nice feature of Store<T>
is that it deduplicates subscribers,
that is you can't accidentally add the same subscriber more than
once to the same store (just like the DOM addEventListener method), although
every time you add it, it will receive the current value:
import {makeStore} from 'universal-stores';
const store$ = makeStore(0);
const subscriber = (v: number) => console.log(v);
const unsubscribe1 = store$.subscribe(subscriber);
const unsubscribe2 = store$.subscribe(subscriber);
const unsubscribe3 = store$.subscribe(subscriber);
console.log(store$.nOfSubscriptions);
unsubscribe3();
unsubscribe2();
unsubscribe1();
console.log(store$.nOfSubscriptions);
If you ever needed to add the same function
more than once you can still achieve that by simply wrapping it inside an arrow function:
import {makeStore} from 'universal-stores';
const store$ = makeStore(0);
const subscriber = (v: number) => console.log(v);
console.log(store$.nOfSubscriptions);
const unsubscribe1 = store$.subscribe(subscriber);
console.log(store$.nOfSubscriptions);
const unsubscribe2 = store$.subscribe((v) => subscriber(v));
console.log(store$.nOfSubscriptions);
unsubscribe2();
console.log(store$.nOfSubscriptions);
unsubscribe1();
console.log(store$.nOfSubscriptions);
Deriving
A derived store is a ReadonlyStore<T>
(see below) whose
value is the result of a computation on one or more
source stores.
Example:
import {makeStore, makeDerivedStore} from 'universal-stores';
const store$ = makeStore(1);
const derived$ = makeDerivedStore(store$, (n) => n + 100);
derived$.subscribe((v) => console.log(v));
store$.set(3);
ReadonlyStore
When you derive a store, you get back a ReadonlyStore<T>
.
This type lacks the set
and update
methods.
A Store<T>
is in fact an extension of a ReadonlyStore<T>
that adds the aforementioned methods.
As a rule of thumb, it is preferable to pass around ReadonlyStore<T>
s,
to better encapsulate your state and prevent unwanted set
s or update
s.
Lazy loading
To create a Store<T>
or a ReadonlyStore<T>
you can use makeStore(...)
or makeReadonlyStore(...)
.
Both these functions take an optional initial value as their first parameter, and
that's their most common use case, but sometimes it could be useful
to lazy load a store or alter its value by using a StartHandler
.
A StartHandler
is a function that gets called whenever the store is activated,
i.e. it gets at least one subscription. If the StartHandler
returns a function,
that function will be called whenever the store is deactivated, i.e.
it has no active subscriptions.
Example:
import {makeReadonlyStore} from 'universal-stores`;
const oneHertzPulse$ = makeReadonlyStore<number>(undefined, (set) => {
console.log('start');
const interval = setInterval(() => {
set(performance.now());
}, 1000);
return () => {
console.log('cleanup');
clearInterval(interval);
};
});
const unsubscribe = oneHertzPulse$.subscribe((time) => console.log(time)); // prints "start" followed by the current time
// for the next five seconds the store will print the current time each second
setTimeout(() => {
// prints "cleanup"
unsubscribe();
}, 5000);