Skip to content
Zap Studio
store
Esc
navigateopen⌘Jpreview
On this page

derive

An auto-tracked, cached computed value built from one or more stores.

derive(deps, fn) creates a computed value from one or more stores (or other derive values). It is cached, and it recomputes only when something it actually read has changed.

Import

import { derive } from "@zap-studio/store";

Basic Usage

const counter = createStore({ count: 2 }, (set) => ({
  increment: () => set((s) => ({ count: s.count + 1 })),
}));

const double = derive([counter], (s) => s.count * 2);

double.get(); // 4
counter.get().increment();
double.get(); // 6

deps gives fn typed, positional arguments — s above is counter’s state, typed as { count: number } with no manual generics.

Multiple Dependencies

Each dependency’s state is passed to fn in the same order as deps:

const a = createStore({ value: 2 });
const b = createStore({ value: 3 });

const sum = derive([a, b], (av, bv) => av.value + bv.value);

sum.get(); // 5

Auto-Tracking

Dependency tracking is automatic and does not stop at deps. While fn runs, every store it reads gets tracked — including one not listed in deps at all:

// `deps` is empty on purpose: fn reads `counter` directly.
const double = derive([], () => counter.getState().count * 2);

double.get(); // tracks `counter` even though it is not in `deps`
counter.get().increment();
double.get(); // recomputes correctly

This also means the dependency set can change between recomputes, if fn reads different stores depending on some condition:

const mode = createStore({ useA: true }, (set) => ({
  toggle: () => set((s) => ({ useA: !s.useA })),
}));

const picked = derive([mode], (m) => (m.useA ? a.getState().value : b.getState().value));

picked.get(); // reads `a`
mode.get().toggle();
picked.get(); // now reads `b` instead — `derive` stops listening to `a`

deps only drives argument order and typing here, not correctness — treat it as documentation of the common-case inputs, not the full list of what fn may read.

Caching

derive’s value is computed lazily and cached: reading it twice in a row without a dependency changing runs fn only once.

const compute = vi.fn((s: { count: number }) => s.count * 2);
const double = derive([counter], compute);

double.get();
double.get();

compute; // called exactly once

derive-of-derive

A derive value can depend on another derive value, so computed values compose:

const double = derive([counter], (s) => s.count * 2);
const quadruple = derive([double], (v) => v * 2);

quadruple.get(); // 8
counter.get().increment();
quadruple.get(); // 12

Subscribing

subscribe(listener) calls listener with the new value, but only when the computed value actually changed (compared with Object.is) — not on every upstream change:

const isEven = derive([counter], (s) => s.count % 2 === 0);

const unsubscribe = isEven.subscribe((value) => console.log(value));

counter.get().increment(); // count: 1, isEven: false -> logs false
counter.get().increment(); // count: 2, isEven: true -> logs true

unsubscribe();

Unsubscribing the last listener also stops listening to every dependency, until the value is read or subscribed to again.

See Also

  • Getting Started — creating a store to derive values from
  • set — how the state a derive reads gets updated

Last updated on September 21, 2026

Was this page helpful?