---
id: "packages/active-state/stores-and-derived"
title: "Stores and derived values"
description: "When a write is accepted and published, how equality decides it, and how derived values stay current."
url: "/packages/active-state/stores-and-derived"
locale: "en"
revision: "dca34d29d8a926d3"
---

# Stores and derived values

## A store holds one value

```ts
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 given
filters.update((current) => ({...current, page: current.page + 1})); // from the current value
filters.state; // {query: 'pen', page: 2}
```

- `setState(next)` stores `next` exactly as given; it never calls it, also when it is a function.
- `update(fn)` stores `fn(current)`. `fn` runs 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

```ts
import {Derived} from '@workspace/active-state';

const visible = new Derived({
  inputs: [filters, products],
  compute: (current, list) =>
    list.filter((product) => product.name.includes(current.query)),
});
```

- `compute` receives the input values in the order of `inputs`, 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 `batch` or 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` (default `Object.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:

```ts
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 cleanup
```

**Example: A store, derived values and useStore**

Client: Runs entirely in your browser. Source: `examples/state/cart-total.tsx`.

```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>
  );
}
```
