Stores and derived values
When a write is accepted and published, how equality decides it, and how derived values stay current.
@workspace/active-state 1.0.1Developmentunreleased, in development in Foundationbaseline ce081dc3
A store holds one value#
import {shallow, Store} from '@workspace/active-state';
const filters = new Store({query: '', page: 1}, {equal: shallow});
filters.setState({query: 'pen', page: 1}); // stores the value as givenfilters.update((current) => ({...current, page: current.page + 1})); // from the current valuefilters.state; // {query: 'pen', page: 2}setState(next)storesnextexactly as given; it never calls it, also when it is a function.update(fn)storesfn(current).fnruns in a pure context: it may read, not write.subscribe(listener)delivers(value, previous)for each published transition and returns a release function.
Equality is yours to choose. Without equal a store accepts and publishes every write, also an
identical value. With equal, a write that is equal to the current value is not accepted and
publishes nothing. shallow compares arrays element by element, plain objects key by key, maps,
sets, dates (by time) and URLs.
A derived value computes from declared inputs#
import {Derived} from '@workspace/active-state';
const visible = new Derived({ inputs: [filters, products], compute: (current, list) => list.filter((product) => product.name.includes(current.query)),});computereceives the input values in the order ofinputs, and nothing else. It must be pure: memory between evaluations belongs in a store.- A read is always current, observed or not, also inside a
batchor transaction. Reading never creates or consumes a notification. - An observed derived value publishes when its settled value differs from the last published one
under its
equal(defaultObject.is). - Declared inputs cannot form a cycle; a compute that reads its own value through a closure throws
DependencyCycleError.
Settle, then publish#
A write marks what depends on it. Subscribers run once per round, after every affected derived value has settled, so a subscriber never sees a new input next to a stale derived value. A write from a subscriber starts a following round.
Effects react to published values#
An Effect runs after the subscribers of a round in which one of its inputs published. It belongs
to a Scope, may return a cleanup, and must be synchronous:
import {Effect, Scope} from '@workspace/active-state';
const scope = new Scope();new Effect({ scope, inputs: [filters], immediate: true, run: ([current]) => { document.title = current.query ? `Search: ${current.query}` : 'Search'; },});
scope.dispose(); // stops the effect and runs its cleanupA store, derived values and useStoreClient
- Notebook: 4.50
1 item, total 4.50
// apps/docs/examples/state/cart-total.tsx'use client';
import {Derived, shallow, Store, useStore} from '@workspace/active-state/react';import {Button} from '@workspace/active-ui/components/primitives/button';
interface Line { product: string; price: number;}
// Module scope: the model lives outside React and needs no provider.const lines = new Store<Line[]>([{product: 'Notebook', price: 4.5}], { equal: shallow,});const total = new Derived({ inputs: [lines], compute: (list) => list.reduce((sum, line) => sum + line.price, 0),});const count = new Derived({inputs: [lines], compute: (list) => list.length});
const PRODUCTS: Line[] = [ {product: 'Pen', price: 1.25}, {product: 'Folder', price: 2}, {product: 'Stapler', price: 7.5},];
/** A Store, two Derived values over it, and useStore in React. */export default function CartTotal() { const items = useStore(lines); const sum = useStore(total); const n = useStore(count); return ( <div className="flex flex-col gap-4"> <div className="flex flex-wrap gap-2"> {PRODUCTS.map((line) => ( <Button key={line.product} variant="outline" size="sm" onClick={() => lines.update((list) => [...list, line])} > Add {line.product} </Button> ))} <Button variant="ghost" size="sm" onClick={() => lines.setState([])} disabled={n === 0} > Empty </Button> </div> <ul className="text-muted-foreground text-sm"> {items.map((line, index) => ( <li key={index}> {line.product}: {line.price.toFixed(2)} </li> ))} </ul> <p className="text-sm" aria-live="polite" data-cart-total> {n} item{n === 1 ? '' : 's'}, total {sum.toFixed(2)} </p> </div> );}