Examples
Every working example of active-state on one page. Each one shows the file that renders it.
@workspace/active-state 1.0.1Developmentunreleased, in development in Foundationbaseline ce081dc3
All examples are Client examples: the model lives in your browser. The search example uses a timer in place of a request, so an earlier query answers later than a newer one.
A store, derived values and useStoreClient
- Notebook: 4.50
1 item, total 4.50
Full source examples/state/cart-total.tsx
// 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> );}A search that a late answer cannot overwriteClient
0 found
Full source examples/state/search-operation.tsx
// apps/docs/examples/state/search-operation.tsx'use client';
import {useState} from 'react';
import { Operation, Scope, Store, useStore, type Outcome,} from '@workspace/active-state/react';import {Input} from '@workspace/active-ui/components/primitives/input';import {Label} from '@workspace/active-ui/components/primitives/label';
const CITIES = [ 'Amsterdam', 'Antwerp', 'Athens', 'Berlin', 'Bern', 'Bratislava', 'Brussels', 'Budapest', 'Copenhagen', 'Dublin',];
/** * A stand-in for a request: it answers after a delay that is longer for a * shorter query, so an earlier query answers after a later one. */function lookUp(query: string, signal: AbortSignal): Promise<string[]> { return new Promise((resolve, reject) => { const timer = setTimeout( () => resolve( CITIES.filter((city) => city.toLowerCase().startsWith(query.toLowerCase()), ), ), 900 - Math.min(query.length, 4) * 200, ); signal.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }); });}
const scope = new Scope();const results = new Store<string[]>([]);const log = new Store<string[]>([]);const search = new Operation<string, string[]>({ scope, policy: 'latest-wins', run: async (query, ctx) => { const found = await lookUp(query, ctx.signal); // The only way to change state; refused once a newer query superseded this one. ctx.commit(() => results.setState(found)); return found; },});
function describe(query: string, outcome: Outcome<string[]>): string { if (outcome.status === 'succeeded') return `"${query}": committed ${outcome.value.length} result(s)`; if (outcome.status === 'superseded') return `"${query}": superseded by #${outcome.by}, nothing committed`; if (outcome.status === 'cancelled') return `"${query}": cancelled (${outcome.reason})`; return `"${query}": failed`;}
/** An Operation with the latest-wins policy: a late answer cannot overwrite a newer one. */export default function SearchOperation() { const [query, setQuery] = useState(''); const found = useStore(results); const status = useStore(search.status); const lines = useStore(log); return ( <div className="flex flex-col gap-3"> <div className="flex flex-col gap-1.5"> <Label htmlFor="state-search">City</Label> <Input id="state-search" value={query} placeholder="Type quickly: B, Br, Bra…" onChange={(event) => { const next = event.target.value; setQuery(next); const invocation = search.invoke(next); void invocation.outcome.then((outcome) => log.update((list) => [`#${invocation.id} ${describe(next, outcome)}`, ...list].slice( 0, 6, ), ), ); }} /> </div> <p className="text-sm" aria-live="polite"> {status.phase === 'running' ? 'Searching…' : `${found.length} found`} {found.length > 0 ? `: ${found.join(', ')}` : ''} </p> <ol className="text-muted-foreground font-mono text-xs" data-state-log> {lines.map((line) => ( <li key={line}>{line}</li> ))} </ol> </div> );}An all-or-nothing transferClient
- Savings
- 100
- Current
- 20
- Total
- 120
Full source examples/state/transfer-transaction.tsx
// apps/docs/examples/state/transfer-transaction.tsx'use client';
import {useState} from 'react';
import { Derived, Store, transaction, useStore,} from '@workspace/active-state/react';import {Button} from '@workspace/active-ui/components/primitives/button';
const savings = new Store(100);const current = new Store(20);const total = new Derived({ inputs: [savings, current], compute: (a, b) => a + b,});
/** Moves `amount`; throws, and so reverts both writes, when savings run short. */function transfer(amount: number) { transaction(() => { current.update((value) => value + amount); savings.update((value) => value - amount); if (savings.state < 0) throw new Error('Not enough savings'); });}
/** A transaction is all-or-nothing: a failed transfer changes neither account. */export default function TransferTransaction() { const [error, setError] = useState<string | null>(null); const a = useStore(savings); const b = useStore(current); const sum = useStore(total); const move = (amount: number) => { try { transfer(amount); setError(null); } catch (thrown) { setError(thrown instanceof Error ? thrown.message : String(thrown)); } }; return ( <div className="flex flex-col gap-3"> <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm"> <dt className="text-muted-foreground">Savings</dt> <dd data-account="savings">{a}</dd> <dt className="text-muted-foreground">Current</dt> <dd data-account="current">{b}</dd> <dt className="text-muted-foreground">Total</dt> <dd>{sum}</dd> </dl> <div className="flex flex-wrap gap-2"> <Button variant="outline" size="sm" onClick={() => move(30)}> Transfer 30 </Button> <Button variant="outline" size="sm" onClick={() => move(500)}> Transfer 500 </Button> </div> <p className="min-h-5 text-sm" role="status"> {error ? `Reverted: ${error}. Neither account changed.` : null} </p> </div> );}