API reference
Every public export of active-state, with its contract - the package's own reference, and every export generated from the source.
First the package's own docs/, included unchanged: the contract of
each export. Then every export of each entry point, generated from the source,
so nothing the package exports is missing here.
Per-export contract for @workspace/, an internal workspace package. This document describes the shape and observable behaviour of every public export; per-symbol TSDoc lives in the TypeScript sources and the compiler is authoritative on types. The decisions behind the contract are in DECISIONS. § Besluit 5, the runtime model is in ARCHITECTURE., and purpose and scope are in FUNCTIONEEL-. The public surface is pinned symbol by symbol in src/__.
Import paths#
| Entry | Contents |
|---|---|
@workspace/ | The framework-independent core. Imports no React |
@workspace/ | The whole core surface again (the same objects), plus use and use (EXPERIMENTAL) |
@workspace/ | Test seams and the instrumentation protocol. Internal and unstable; for tests and diagnostics |
A React consumer imports from / only. react@^19 is an optional peer, required by / and by the React-based parts of a test setup.
Surface at a glance#
| Kind | Exports |
|---|---|
| Classes | Store, Derived, Effect, Operation, Scope |
| Functions | batch, transaction, shallow |
| Errors | Purity, Structural, Lifecycle, Disposed, Dependency, Commit, Commit, Feedback |
| Types | Source, Readable, Listener, Unsubscribe, Store, Derived, Values, Effect, Effect, Scope, Scope, Operation, Operation, Operation, Invocation, Invocation, Invocation, Outcome, Cancel, Policy, Not, Guarded |
/ only | use, Use, use (EXPERIMENTAL), Scoped |
Contexts: which calls are allowed where#
Every mutating or lifecycle call first checks where it runs. The rules apply to every section below.
| Context | Allowed | Rejected |
|---|---|---|
| Pure: Store updaters, every equality, Derived compute, probe listeners | Reads (., invocation., scope.), shallow | Everything else, with Purity |
Open Transaction (transaction, ctx.) | Reads, set/update, nested transaction/batch, unowned Store/Derived construction, invoke() (deferred) | Structural and lifecycle calls (subscribe, releasing a handle, any dispose, Scope calls, scoped construction, Effect/Operation construction, cancel), with Structural; ctx. with Commit |
| Callback or plain code | Everything | — |
A rejection inside an open Transaction fails that Transaction even when user code catches the error.
Store<T>#
The writable state authority. One type parameter; writing is invariant in T.
new Store<T>(initial: T, options?: StoreOptions<T>)
interface StoreOptions<T> { equal?: (a: T, b: T) => boolean; // "unchanged"; no default (see below) label?: string; // value-free identifier for instrumentation scope?: Scope; // disposed with this Scope}The current value. O(1), no side effect. Inside a Transaction it holds the Transaction's own writes
Stores next exactly as given; never calls it, whatever its type. A function is an ordinary value
Stores fn(current). fn runs in a pure context; if it throws, nothing is accepted and the error reaches the caller
Registers (value, previous) => void for published transitions. Each call is its own registration with an idempotent release; the same function subscribed twice is delivered twice
A memoised read-only projection typed Source<T>: the same value and transitions, no write members at type level or at runtime. It is a native source, so it can be a Derived or Effect input
Makes the Store inert: subscriptions released without notification, later writes ignored, reads return the frozen value. Idempotent
Acceptance. A write passes equal(current, candidate) when equal is set; an equal candidate is not accepted and publishes nothing. An accepted write is stored before the call returns, and no user callback runs between acceptance and storage.
Publication. A Store with an accepted write publishes at the next publication boundary, when it has no equal, or when !equal(last. Subscribers receive (value, previous), where previous is the last published value; unpublished intermediate values never become previous.
No default equality. A Store without equal accepts and publishes every write, also an identical value. Whether a default should exist (Object. or shallow) is an open decision (ROADMAP R7). Supply equal explicitly where you rely on it.
Disposed Stores ignore writes silently; with an instrumentation listener attached, each ignored call is a write. event (see /).
Readables: Source<T>, Readable Store<T>, Listener<T>, Unsubscribe#
interface ReadableStore<T> { readonly state: T; subscribe(listener: Listener<T>): Unsubscribe;}type Listener<T> = (value: T, previous: T) => void;type Unsubscribe = () => void;Readableis the smallest readable shape.Store<T> useaccepts any value that satisfies it.Store Source<T>is a native graph source: aStore, a readonly projection, aDerivedor an Operation'sstatus. It is exported as a type only; the runtime recognises a source by a private brand, not by its shape, so a cast or impostor is rejected as an input.Source<T>is covariant and exposes onlystateandsubscribe.
Derived<T, I>#
Computed state over explicit inputs.
new Derived({ inputs: [a, b], // native sources, copied and frozen at construction compute: (valueA, valueB) => …, // positional input values; pure and deterministic equal?: (x, y) => boolean, // default Object.is label?: string, scope?: Scope,});
type ValuesOf<I extends readonly Source<unknown>[]> = { readonly [K in keyof I]: I[K] extends Source<infer V> ? V : never;};I is inferred from inputs (a const type parameter), so no as const is needed; Derived<T> alone names any Derived of T.
Always the value for the current inputs, observed or not, also inside an open batch or Transaction. Construction computes nothing; the first read or observation does
Observes the Derived: the first observer activates it (with catch-up), the last release deactivates it
Releases subscriptions, stops observing inputs and freezes the last value
Freshness and publication are independent. A read never creates, suppresses or consumes a notification. An observed Derived publishes when its settled value differs from its last published value under
equal; the same command notifies the same observers with or without intermediate reads.A Derived is a value, not a reducer:
computereceives the input values and nothing else. Memory across evaluations belongs in a Store.Failure. A throwing
computeorequalreaches the reader; nothing is cached and a later read retries. During settlement the failure goes to the error sink and the node and its dependents publish nothing that round.Cycles. Declared inputs cannot form a cycle. A compute that reads its own Derived through a closure throws
Dependency. A read of an undeclared source is not tracked (unsupported; visible asCycle Error derived.with a listener).undeclared- read Disposed. Reads return the frozen value; a Derived disposed before it ever produced a value throws
Disposed.Resource Error
Scope#
A lifetime container.
new Scope(options?: {parent?: Scope; pending?: boolean; label?: string})type ScopeState = 'pending' | 'active' | 'disposing' | 'disposed';The current Scope
Tears down every member in reverse registration order (a child Scope completely at its own position), attempts every step, then throws the failure (one error as itself, several as Aggregate). Publishes nothing. Idempotent
Registers a disposer, called once at its position
Activates a pending model from its root, atomically: every deferred acquisition (observations, Effect starts, queued invocations) succeeds, or all are undone, the model stays pending and the error is rethrown. Rejected with Lifecycle on a non-root pending Scope
Ownership. Stores and Deriveds may be unowned; Effects and Operations always have a Scope. Individually disposed members leave their Scope at once.
Pending Scopes (
pending: true, or any child of a pending parent) are acquisition-free: nothing they own starts an Effect, admits an invocation or observes an outside source until the root is activated. A pending owner only delays the start: an invocation made inside a Transaction orctx.that reverts endscommit cancelled('transaction-and a later activation does not start it. A discarded pending model is referenced by nothing in the runtime. A factory that builds one must only construct resources;reverted') fetch, direct external subscriptions and DOM work belong in an Effect or Operation.Disposed resources are inert: writes are ignored,
invokereturns an already cancelled invocation,subscribereturns a no-op handle, reads return the frozen value. Two calls still throw: a call from a pure context, and reading a Derived that was disposed before it ever produced a value (Disposed).Resource Error
Effect<I>#
An owned reaction to published input transitions. Not a Derived and not React's use.
new Effect({ scope, // required inputs: [a, b], run: ([valueA, valueB], ctx) => { …; return () => {/* cleanup */}; }, immediate?: boolean, label?: string,});interface EffectContext { readonly scope: Scope } // the run ScopeWhen it runs. In the reaction phase of a round in which at least one input published, after every subscriber of that round, with the complete input tuple captured before the round's first callback. Runs never nest. If an input failed or stayed undecided in that round, the Effect is skipped.
immediate. The Effect is owed an initial run. It never begins before the constructor has returned; it runs at the first publication boundary after the start (a later write, the close of an enclosingbatch/Transaction, a later round, or the activation of a pending Scope). No microtask or timer is introduced.Run Scope. Each run receives
ctx.. Resources created in it live until the run ends, and only the Effect disposes it.scope Teardown order, on every path (rerun,
dispose(), owner disposal, disposal during the run): the cleanup, then the run Scope, then, when the Effect stops, unobserving the inputs.Synchronous only. A run or cleanup that returns a promise is a diagnosed misuse (
Typeto the error sink or theError dispose()caller). It is never awaited, and its rejection is reported, never left unhandled. The type cannot rule out an async cleanup; the runtime check is the guarantee.Failures of a run or cleanup go to the error sink; the Effect stays started and runs again on its next trigger.
Operation<TInput, TResult>#
Owned in-flight work with identity, policy, status and typed outcomes.
new Operation<TInput = void, TResult = unknown>({ scope, // required; disposal cancels everything policy: 'latest-wins' | 'drop-while-running' | 'queue', run: (input, ctx) => PromiseLike<TResult> | TResult, label?: string,});Creates an Invocation. It never runs run inline: outside every boundary it is admitted before invoke() returns; inside an open Transaction, batch, running turn or pending Scope it waits for every one of those gates. Made inside a Transaction (or a ctx.) it belongs to that frame whatever its owner's state: a revert cancels it with transaction-, a commit hands it to the next gate. On a disposed Operation it returns an invocation already cancelled('owner-. For TInput = void it takes no argument
A native Source<Operation: {phase: 'idle' | 'running', running, queued, last}. Usable as a Derived or Effect input and with use. Written only by the runtime, never inside a Transaction
Cancels every invocation with owner-, aborts running signals and freezes the status
Policies. latest-: a new admission supersedes the running invocation. drop-: a new invocation is cancelled('dropped') while one runs. queue: FIFO, one at a time. "Latest" is creation order.
Invocation<TResult>: id (per Operation, in creation order), status ('queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'superseded'), outcome (a promise that never rejects), cancel() (requested; no effect once ended; rejected inside an open Transaction).
Outcome<TResult>: {status: 'succeeded', value}, {status: 'failed', error: unknown} (the original thrown or rejected value), {status: 'cancelled', reason: Cancel or {status: 'superseded', by: Invocation. Cancel is 'requested' | 'owner-. Cancellation and supersession are outcomes, not failures.
Operation (the ctx of run):
signal: anAbort, aborted when the invocation is cancelled or superseded.Signal invocation: itsInvocation.Id commit(fn): runsfnas an atomic commit authorized for this invocation. Result, in this order: not current at entry →fndoes not run,false; currency lost during the commit → everything reverted,false; otherwise a failure inside it (also one user code caught) → everything reverted and that first failure rethrown; otherwise committed,true. Cancelling or disposing from inside a commit is rejected and reverts it. A commit inside another open Transaction throwsCommit. A callback that visibly returns a promise does not compile.Nesting Error is: whether a commit would be admitted now. Not a substitute forCurrent() commit.
The stale-write guarantee covers ctx. only. A direct Store write from the async body after an await is unsupported and unprotected; nothing can attribute it to the invocation. Late settlements after a terminal outcome are absorbed, never unhandled.
batch(fn) and transaction(fn)#
function batch<T>(fn: () => NotThenable<T>): T;function transaction<T>(fn: () => NotThenable<T>): T;type NotThenable<T> = T extends PromiseLike<unknown> ? never : T;batch groups publication only. Writes inside are accepted and readable at once; nothing is published until the outermost batch (or open Transaction) closes, and then synchronously, before batch returns. It never reverts anything: if fn throws, its writes survive, are published, and then the error is rethrown. Nested batches deepen the grouping.
transaction is synchronous, local and all-or-nothing:
Writes happen in place, so reads inside see them, for Stores and Deriveds alike.
A nested
transactionjoins the open one: one frame, no savepoints.The frame fails when a failure crosses an active-state boundary inside it (a nested
transactionorbatchthat throws, a throwing updater or equality, a failing Derived read, a rejected call), also when user code catches it;fnthrowing fails it too. A plain throw caught entirely inside user code does not.A failed frame restores every Store and Derived cache it touched, publishes nothing of its own work (whatever the equality) and rethrows its first failure. A committed frame publishes when the outermost hold closes, before the outermost call returns.
Structural and lifecycle calls are rejected inside it.
invoke()is deferred: admitted after commit (or, for an Operation whose Scope is pending, after that Scope's activation),cancelled('transaction-after a revert, whatever its owner's state.reverted') Only local state is reverted; network requests, DOM changes or copies made to other variables are not.
Nesting. A Transaction inside a batch is atomic on its own; the batch's earlier writes survive its failure. A batch inside a Transaction shares its publication, and an error escaping the batch fails the Transaction.
Synchronous only. A callback that returns a thenable is refused with a Type (the Transaction reverts; a batch publishes its writes first), and a rejection handler is attached so nothing becomes unhandled. Not catches direct async callbacks at compile time; it cannot see through unknown, void, wide types or generic callbacks, and the runtime check is the guarantee.
shallow(a, b)#
Shallow equality over an explicit set of value families: Object.; arrays (same length, same holes, element-wise Object.); Maps and Sets; Dates by timestamp; URLs by href; two objects with the same prototype and the same own keys (strings and symbols, enumerable or not) with Object. values; two empty plain objects. Everything else is not equal: an instance that owns no property (a Promise, a Weak, a class keeping its state in private fields or prototype getters) is never called equal to another instance. Symmetric for deterministic values; reads own properties, so an own getter or proxy trap runs and may throw. It compares one level deep. It is the default equality of use and the usual equal for an object- or array-valued Store.
Errors#
All are Error subclasses with a matching name.
| Error | Thrown when |
|---|---|
Purity | A mutation or lifecycle call from a pure context (call: Guarded names it) |
Structural | A structural or lifecycle call inside an open Transaction (call names it); it also fails that Transaction |
Lifecycle | A lifecycle call the lifetime model forbids, such as activating a non-root pending Scope |
Disposed | Reading a Derived that was disposed before it ever produced a value |
Dependency | A Derived is demanded during its own demand (a compute or equality reads it through a closure) |
Commit | ctx. inside another open Transaction or commit |
Commit | A Store write inside a commit whose invocation lost its authority |
Feedback | Reported to the error sink when a turn exhausts its budget of 100 steps; the remaining work is suspended, not dropped, and resumes at the next publication boundary |
Where failures go. Failures of your own call (an updater, an acceptance equality, a failing read, a batch/transaction callback, a rejected call) reach you synchronously. Failures without a synchronous caller (subscribers, Effect runs and cleanups, settlement, publication decisions, the budget) are collected per turn and reported once when the turn ends: one error as itself, several as an Aggregate, to the error sink. The default sink calls global where available and otherwise rethrows in a microtask. They do not reach React Error Boundaries. Tests replace the sink through /.
use Store(readable, selector?, options?) — / react#
function useStore<T>( readable: ReadableStore<T>, selector?: undefined, options?: UseStoreOptions<T>,): T;function useStore<T, S>( readable: ReadableStore<T>, selector: (state: T) => S, options?: UseStoreOptions<S>,): S;interface UseStoreOptions<S> { readonly equal?: (a: S, b: S) => boolean;} // default shallowuseand nothing more: the snapshot isSync External Store readable.read during render; React subscribes after commit and releases on cleanup. Subscribing to a Derived activates it; the last release deactivates it. Passing a different readable replaces the subscription.state Selection cache. A retained selection is reused only for the same readable and the same equality function; it is returned without re-selecting only when the selector and the snapshot are identical too, and kept by reference when
equalcalls the new selection equal. An inline selector that builds a new object every render does not loop.The selector and
equalrun during render and must be pure.The server snapshot is the same getter; it is consistent only where the host supplies the same per-request initial data to server and client. No Provider or context is read.
use Scoped(factory) — / react, EXPERIMENTAL#
type ScopedFactory<T> = (scope: Scope) => T;function useScoped<T>(factory: ScopedFactory<T>): T;Owns a model for the committed lifetime of a component. Experimental: exported after its exit gate passed (EXP-01 … EXP-08, see ARCHITECTURE. § 11) and the export was approved (R14, T8-1). It has one production consumer and stays experimental: its shape and behaviour may still change with evidence from further use.
Supported host (the evidence). React 19.3.0 with react-dom 19.3.0 (create, hydrate), development and production builds, in Chromium (through playwright-) and in jsdom. Other React versions are not claimed.
Lifecycle contract.
Render. The first render calls
factorywith a pending Scope. A render that is discarded, or rendered twice in development, builds a model that never starts anything and that nothing references.Commit. The model is activated in the layout phase of the commit that keeps it, before any passive subscription of that commit.
End. When React tears the component's effects down (unmount, or a hidden
Activity), the component's subscriptions are released at once and it does not render again for the model. The model itself is disposed at the next microtask checkpoint; from then on its Effects are stopped, its invocations endcancelled('owner-and later commits are refused. A developmentdisposed') Strictrehearsal sets the effects up again before that checkpoint and keeps the model. A tree hidden by a re-suspending Suspense boundary keeps its model.Mode The grace before the disposal is not side-effect-free. Until the checkpoint the model's Scope is still
active: a continuation queued ahead of it still runs and may commit, and the model's own Effects react to that commit, including any external work they do. Only the disposal ends the model's authority.Identity. One model per component lifetime; new props do not rebuild it, so a consumer that needs a new model remounts with a
key. The one exception is a component whose model was disposed while React kept its state (a hiddenActivityshown again): it gets a new model from the latest factory; the old one stays disposed.After the end. A disposed model's lifetime is over, whether it ended with its component or was replaced: its Operations never regain commit authority, and its subscriptions and Effects are released. A command still invoked on it, such as a handler that captured it, obeys the disposed-resource rules of
Scope: writes are ignored and invocations end cancelled, but reading a model Derived that was never evaluated throwsDisposed(R12). Consumers avoid invoking commands on an ended model, for example by first checking that its Scope is stillResource Error active.A failing activation reaches the nearest error boundary; the model stays pending and ends with the component.
Factory obligations. The factory runs during render and must only construct resources in the Scope it receives: Stores, Deriveds, Effects, Operations, child Scopes, and invocations of its own Operations. It must not fetch, subscribe to an outside source directly, write outside state or touch the DOM; that work belongs in an Effect or Operation of the model. A factory that disposes its Scope gets a Lifecycle.
Known limits.
The absence of React subscribers is no guard. A side effect that must not outlive the component bases its decision on the model's ownership where it acts: an outcome that
succeededbefore unmount stayssucceeded, so a side effect after an awaited outcome, such as navigation, checks at that point that the model's Scope is stillactive; during the grace the Scope still reportsactive.A settlement failure of an observed Derived is not a React update; it surfaces through the error sink, and in the component at its next render.
The testing entry: / testing#
Test seams that work on the core without React, and the internal, unstable instrumentation protocol. Not for production code paths. No export gives access to a revision, depth, edge list or private member.
Installs a capturing error sink. take() returns the captured errors and marks them asserted; release() restores the previous sink and throws Unasserted if a captured error was never taken
Replaces the sink until the returned function is called (prefer capture)
Records the instrumentation stream until detach(): events, of, about(resource), id
The published resources per round, by label, sorted within a round, for read/no-read comparisons
Plain descriptions: state, live members by kind, subscription and observer counts, lifecycle state, live invocations
Direct observers of a native source; acquisitions a pending barrier holds
Pending work, queued admissions and the suspended backlog; resume a backlog deterministically
A run whose invocations are held until the test resolves or rejects each by id; exposes started ids, inputs, contexts, abort state, and prefix and abort-listener injection
Cancels as an unexpected terminal transition would, bypassing the guards, to exercise commit revocation
Instrumentation events carry a sequence number, a type from a closed set, detached resource and Scope references (identifiers and labels), a cause chain (direct, batch, transaction, operation, effect, subscriber, disposal, each with its parent) and value-free detail. They never contain state values, diffs, error objects, stacks or timings. Without a listener nothing is produced. Misuse diagnostics are events (write., derived., lifetime., turn.); the runtime itself never logs, warns or reads process.. A development or test setup that wants warnings attaches a listener through record().
What is proved, and what is not#
Core and
use: Node/jsdom behaviour tests underStore src/, including a real**/__ tests__/ React.wrapper, a suspended transition,Strict Mode hydratewith matching data, server rendering and readable replacement. The tests are the executable specification of this document.Root use: the eight exit criteria in jsdom and in Chromium on the host stated above.Scoped Not claimed: other React versions, React Compiler compatibility, serialization or hydration of state (the host supplies consistent data), and every browser beyond Chromium.
All exports, from the source#
@workspace/ active- state#
Functions, hooks and constants
- Type
<T>(fn: () => NotThenable<T>) => T
Groups publication: writes made inside fn are accepted and readable at once, and nothing is published until the outermost batch (or open Transaction) closes. Closing the outermost hold publishes synchronously, before batch returns or rethrows. batch never reverts anything: if fn throws, its writes survive and are published, then the error is rethrown. Inside an open Transaction, an error escaping fn fails that Transaction. Returns fn's value.
- Type
<T>(fn: () => NotThenable<T>) => T
Runs fn atomically. Writes are made in place, so reads inside see them (Stores and Deriveds alike), and nothing is published while it runs. A nested transaction joins the open one: there is one frame and no savepoint. The frame fails when a failure crosses an active-state boundary inside it (a nested transaction or batch that throws, a throwing updater or equality, a failing Derived read, a rejected call), also when user code catches it; fn throwing fails it too. A failed frame restores every Store and Derived cache it touched, publishes nothing of its own work and rethrows its first failure; a committed one publishes when the outermost hold closes, before the outermost call returns. Structural and lifecycle calls are rejected inside it. Returns fn's value.
- Type
<T>(objA: T, obj B: T) => boolean
Shallow equality over an explicit set of value families.
Equal when:
Object.already says so (identity,is Na, and every primitive);N - both are arrays with the same length, the same holes, and element-wise
Object.values;is - both are
Maps with the same size, the same keys, andObject.values;is - both are
Sets with the same size and the same members; - both are
Dates with the same timestamp; - both are
URLs with the samehref; - both share a prototype and have the same own property keys — strings and symbols, enumerable or not — with
Object.values;is - both are empty plain objects (prototype
Object.orprototype null).
Everything else is NOT equal, and deliberately so. An instance that owns no property — a Promise, a Weak, a class that keeps its state in private fields or exposes it only through prototype getters — is opaque: two distinct opaque instances are never called equal, because nothing on them can show that they are. Prototype getters are not read, and two objects with different prototypes are never equal however well their keys line up. This comparator decides whether a component re-renders; an invented equality hides a state change.
Every rule compares both values the same way, so for deterministic values shallow(a, b) === shallow(b, a). A getter or proxy trap whose answer changes between reads is outside that promise.
Comparison reads own properties, so an own getter or a proxy trap runs, and one that throws reaches the caller. That is deliberate too: absorbing the failure would answer a question this function could not answer.
Nothing is compared more than one level deep: nested objects and arrays are compared by identity.
Owns the lifetime of the resources registered with it. Disposal is complete, ordered (reverse registration, a child Scope torn down at its own position) and idempotent. A pending Scope strongly owns its pending children, but its model root is not referenced from outside until it is activated.
class Scope {
constructor(options?: ScopeOptions);
get state(): ScopeState;
activate(): void;
dispose(): void;
onDispose(disposer: () => void): () => void;
}Client-owned work in flight, with identity, an owner, a status and a concurrency policy. Each invoke() creates an invocation that is admitted by the runtime, never started inline: behind an open Transaction it waits for the commit (and is cancelled transaction- on a revert), behind a batch for its publication boundary, in a pending Scope for the model's activation. A pending owner delays the start only: an invocation made inside a Transaction that reverts is cancelled whatever its owner's state. Admission applies the policy and starts the run.
A stale invocation cannot change newer state through ctx.: a commit takes effect only while its invocation is current for the whole commit. Disposing the owning Scope (or the Operation) cancels every invocation with owner-, aborts running signals and freezes the status.
class Operation<TInput = void, TResult = unknown> {
readonly status: Source<OperationStatus<TResult>>;
constructor(options: OperationOptions<TInput, TResult>);
invoke(...args: [
TInput
] extends [
void
] ? [
] : [
input: TInput
]): Invocation<TResult>;
dispose(): void;
}Computed state over explicit inputs. Construction computes nothing; the first read or observation does. A read always returns the value for the current inputs, observed or not. While observed (subscribed, or an input of an observed Derived) it is maintained by write-time invalidation and publishes when its settled value differs from its last published value under equal; reading it never creates or consumes a publication.
class Derived<T, const I extends readonly Source<unknown>[] = readonly Source<unknown>[]> extends Source<T> {
constructor(options: DerivedOptions<T, I>);
get state(): T;
subscribe(listener: Listener<T>): Unsubscribe;
dispose(): void;
}An owned, lifecycle-bound reaction. It has no value and publishes nothing: it observes its explicit inputs directly and runs in the reaction phase of a round in which one of them published, after every subscriber of that round, with the complete tuple of its input values captured before the round's first callback. A round in which an input failed to settle or to be decided does not run it.
Runs never nest: a write made by a run is published in a later round. Each run gets a fresh run Scope; a run ends exactly once, in one order, however it ends: its cleanup, then its run Scope's disposal, and when the Effect stops, the release of its inputs. The Effect is the only owner of that order; its Scope's disposal reaches the run Scope through the Effect.
Constructed in an active Scope it starts at once (it observes its inputs); in a pending Scope it starts when the model is activated. Construction never runs the Effect: an immediate Effect's initial run waits for the first publication boundary after the start. Construction is rejected in a pure context and inside an open Transaction.
class Effect<const I extends readonly Source<unknown>[] = readonly Source<unknown>[]> {
constructor(options: EffectOptions<I>);
dispose(): void;
}The writable state authority. Its value slot is the only authority for its value: a write is accepted and stored before the call returns, and no user callback runs between acceptance and storage.
No default equality is applied: whether a Store without equal should get one is an open product decision, so today every write without equal is accepted and published.
class Store<T> extends Source<T> {
constructor(initial: T, options?: StoreOptions<T>);
get state(): T;
readonly setState: (next: T) => void;
readonly update: (fn: (current: T) => T) => void;
subscribe(listener: Listener<T>): Unsubscribe;
asReadonly(): Source<T>;
dispose(): void;
}Thrown when an Operation commit is attempted while another atomic frame is open: a commit owns its own authorized frame and never joins someone else's. The rejection fails that open frame.
class CommitNestingError extends Error {
constructor();
}Thrown by a Store write inside a commit whose invocation lost its authority while the commit was open. Nothing of such a commit survives.
class CommitRevoked extends Error {
constructor();
}Thrown when a Derived is demanded while its own demand is still in progress: its compute or equality reads it again through a closure. Declared inputs cannot form a cycle, because every input exists before its dependent.
class DependencyCycleError extends Error {
constructor();
}Thrown by a read that would need a value a disposed resource never had: a Derived disposed before its first evaluation cannot compute after disposal.
class DisposedResourceError extends Error {
constructor(message: string);
}Reported to the error sink when a turn exhausts its step budget. The work that was still pending is retained as the suspended backlog, not dropped.
class FeedbackLimitExceeded extends Error {
readonly steps: number;
constructor(steps: number);
}Thrown when a lifecycle call is made on a resource whose lifetime model does not allow it, such as activating a pending Scope that is not the root of its pending model.
class LifecycleViolation extends Error {
constructor(message: string);
}Thrown when an active-state mutation or lifecycle call is made from a pure context: a Store updater, any equality function, or an instrumentation listener. The rejected call changes nothing.
class PurityViolation extends Error {
readonly call: GuardedCall;
constructor(call: GuardedCall);
}Thrown when a structural or lifecycle call is made inside an open Transaction: subscribing or releasing, disposing, activating, registering a disposer, or constructing an owned resource. A Transaction reverts local state only, so it never has to restore lifetime or observation topology; the rejected call changes nothing and fails the Transaction.
class StructuralMutationInTransaction extends Error {
readonly call: GuardedCall;
constructor(call: GuardedCall);
}Types
A callback result the compiler can see is not a promise or other thenable. It cannot see through unknown, void, a wide declared type or a generic callback; the runtime check of batch and transaction is the guarantee.
type NotThenable<T> = T extends PromiseLike<unknown> ? never : T;A subscriber receives the captured transition of its round.
type Listener<T> = (value: T, previous: T) => void;The smallest readable shape: the current value plus a synchronous subscription with an idempotent release. Every native source satisfies it; a value that merely satisfies it is readable (by the React adapter, for instance) but is not a native source and cannot be a Derived input.
interface ReadableStore<T> {
readonly state: T;
subscribe(listener: Listener<T>): Unsubscribe;
}A native graph source: a readable the runtime created itself (a Store, its readonly projection, a Derived). The graph record is private; whether a value is a native source is decided by that private field, never by its structural shape, so a cast, an impostor or an object that merely inherits from a source is not one. Covariant: a source of a narrower value is a source of a wider one.
abstract class Source<out T> {
protected constructor();
abstract get state(): T;
abstract subscribe(listener: Listener<T>): Unsubscribe;
}- Type
type Unsubscribe = () => void;
Releases one subscription. Idempotent.
interface ScopeOptions {
readonly parent?: Scope;
readonly pending?: boolean;
readonly label?: string;
}type ScopeState = 'pending' | 'active' | 'disposing' | 'disposed';Why an invocation was cancelled: its handle asked for it, its owner was disposed, the policy dropped it while another one ran, or the atomic frame it was created in reverted.
type CancelReason =
'requested' | 'owner-disposed' | 'dropped' | 'transaction-reverted';One call of an Operation: inspection and control.
interface Invocation<TResult> {
readonly id: InvocationId;
readonly status: InvocationStatus;
readonly outcome: Promise<Outcome<TResult>>;
cancel(): void;
}- Type
type InvocationId = number;
Identifies an invocation within its Operation, in creation order.
The lifecycle of an invocation: queued until it is admitted and its run starts, running until its run settles, then exactly one terminal outcome.
type InvocationStatus =
'queued' | 'running' | Outcome<unknown>['status'];What a run receives besides its input.
interface OperationContext {
readonly signal: AbortSignal;
readonly invocation: InvocationId;
commit<R>(fn: () => NotThenable<R>): boolean;
isCurrent(): boolean;
}interface OperationOptions<TInput, TResult> {
readonly scope: Scope;
readonly policy: Policy;
readonly run: (
input: TInput,
ctx: OperationContext,
) => PromiseLike<TResult> | TResult;
readonly label?: string;
}The status of an Operation's policy: whether an invocation runs, how many wait in its queue, and the outcome of the last invocation that reached admission. Invocations still waiting for their gate (an open Transaction or batch, a pending owner) are not part of it.
interface OperationStatus<TResult> {
readonly phase: 'idle' | 'running';
readonly running: InvocationId | null;
readonly queued: number;
readonly last: Outcome<TResult> | null;
}How an invocation ended. It never rejects: cancellation and supersession are outcomes, not failures, and a failure carries the original thrown or rejected value, unchanged.
type Outcome<TResult> =
| {readonly status: 'succeeded'; readonly value: TResult}
| {readonly status: 'failed'; readonly error: unknown}
| {readonly status: 'cancelled'; readonly reason: CancelReason}
| {readonly status: 'superseded'; readonly by: InvocationId};How an Operation treats a new invocation while another one is running: latest- supersedes the running one, drop- cancels the new one (dropped), queue runs them one at a time in creation order.
type Policy = 'latest-wins' | 'drop-while-running' | 'queue';interface DerivedOptions<T, I extends readonly Source<unknown>[]> {
readonly inputs: I;
readonly compute: (...values: ValuesOf<I>) => T;
readonly equal?: (a: T, b: T) => boolean;
readonly label?: string;
readonly scope?: Scope;
}The value types of an input list, position by position.
type ValuesOf<I extends readonly Source<unknown>[]> = {
readonly [K in keyof I]: I[K] extends Source<infer V> ? V : never;
};What one run of an Effect receives besides its input values.
interface EffectContext {
readonly scope: Scope;
}interface EffectOptions<I extends readonly Source<unknown>[]> {
readonly scope: Scope;
readonly inputs: I;
readonly run: (
values: ValuesOf<I>,
ctx: EffectContext,
) => void | (() => void);
readonly immediate?: boolean;
readonly label?: string;
}interface StoreOptions<T> {
readonly equal?: (a: NoInfer<T>, b: NoInfer<T>) => boolean;
readonly label?: string;
readonly scope?: Scope;
}The active-state operations that change state or lifetime. A guarded entry point names itself with one of these when it rejects a call.
type GuardedCall =
| 'setState'
| 'update'
| 'subscribe'
| 'unsubscribe'
| 'dispose'
| 'construct'
| 'activate'
| 'onDispose'
| 'resume'
| 'batch'
| 'transaction'
| 'invoke'
| 'cancel'
| 'commit';@workspace/ active- state/ react#
The root entry again, plus:
Functions, hooks and constants
- Type
<T>(factory: ScopedFactory<T>) => T
EXPERIMENTAL. Owns a model for the committed lifetime of a component.
- Render. The first render calls
factorywith a pending Scope. A render that is discarded, or rendered twice in development, builds a model that never starts anything and is referenced by nothing. - Commit. The model is activated in the layout phase of the commit that keeps it, before any passive subscription of that commit, so a component that reads a model Derived subscribes to an active Derived.
- End of the lifetime. When React tears the component's effects down (unmount, or a hidden
Activity), the component's subscriptions are released at once and it does not render again for the model. The model itself is disposed at the next microtask checkpoint. Until then its Scope is stillactive: a continuation queued ahead of the checkpoint still runs and may commit, and the model's own Effects react to that commit, side effects included. The grace is not side-effect-free. From the checkpoint on its Effects are stopped, its invocations endcancelled('owner-and no later commit takes effect. A developmentdisposed') Strictrehearsal sets the effects up again before that checkpoint, which cancels the disposal. A tree hidden by a re-suspending Suspense boundary keeps its passive effects, so its model stays active.Mode - Replacement. A model is never recreated to follow new props: the factory is read once per model, and a consumer that needs a new model remounts the component with a
key. The one exception is a component whose model was disposed while React kept its state (a hiddenActivitythat is shown again): it gets a new model from the latest factory; the old one stays disposed. - After the end. A disposed model's lifetime is over, whether it ended with its component or was replaced: its Operations never regain commit authority, and its subscriptions and Effects are released. A command still invoked on it, such as a handler that captured it, obeys the disposed-resource rules: writes are ignored and invocations end cancelled, but reading a model Derived that was never evaluated throws
Disposed. Consumers avoid invoking commands on an ended model, for example by first checking that its Scope is stillResource Error active.
Nothing stops a factory from performing untracked side effects; see Scoped. The absence of React subscribers is no guard: an external side effect that must not outlive the component, such as navigation after an awaited outcome, decides on the model's ownership where it acts (its Scope still active, the invocation's outcome), knowing that the Scope still reports active during the grace before the disposal checkpoint.
Reads any readable (a Store, its readonly projection, a Derived, an Operation status, or any value that satisfies Readable) and re-renders the component when a published transition changes the selection.
It is React's external-store subscription and nothing more: the snapshot is readable., read during render, so a render always sees the current value; the subscription is taken by React after commit and released by React's cleanup (subscribing to a Derived activates it, the last release deactivates it); a different readable replaces the subscription. The runtime decides what is published and when, so a batch, a Transaction and an Operation commit reach the component only through their publication, and a reverted Transaction never does. No Provider or context is read.
The server snapshot is the same getter: it is consistent only where the host supplies the same per-request initial data to the server and the client.
{ <T>(readable: ReadableStore<T>, selector?: undefined, options?: UseStoreOptions<T>): T; <T, S>(readable: ReadableStore<T>, selector: (state: NoInfer<T>) => S, options?: UseStoreOptions<S>): S; }Types
- Type
type ScopedFactory<T> = (scope: Scope) => T;
Builds a model inside the Scope it is given. It runs while React renders, possibly for a render that never commits and possibly twice, so it must only construct resources: Stores, Deriveds, Effects, Operations and child Scopes in that Scope, and invocations of its own Operations. While the model is pending none of them starts or observes anything outside it. What the runtime cannot see is the factory's own obligation: it must not fetch, subscribe to an outside source directly, write outside state or touch the DOM; such work belongs in an Effect or an Operation of the model.
interface UseStoreOptions<S> {
readonly equal?: (a: S, b: S) => boolean;
}@workspace/ active- state/ testing#
Functions, hooks and constants
- Type
(sink: ErrorSink) => () => void
Replaces the error sink until the returned function is called. Prefer capture, which also fails on unasserted errors.
Thrown by Error when captured errors were never taken.
class UnassertedErrors extends AggregateError {
constructor(errors: readonly unknown[]);
}- Type
() => ErrorCapture
Installs a capturing sink. Reports are delivered synchronously when a turn ends, so what a scenario caused is capturable directly after it ran.
The resources published per round, by label (or kind when unlabelled), sorted within each round: order between the resources of one round is not promised. Two recordings of the same command with and without intermediate reads must give the same summary.
(events: readonly InstrumentationEvent[]) => string[][]- Type
() => Recorder
Records the instrumentation stream until detached.
- Type
(scope: Scope) => ScopeInspection
- Type
<T>(store: Store<T>) => StoreDescription
- Type
<T>(derived: Derived<T>) => DerivedDescription
The lifecycle of an Effect (created, started, stopping, stopped) and whether it holds the Scope of a run that has not ended. No input, run record or cleanup is exposed.
<I extends readonly Source<unknown>[]>(effect: Effect<I>) => EffectDescription- Type
<T>(derived: Derived<T>) => number
How many refused observation acquisitions derived holds while its pending model has not activated. A plain count; no dependent or edge is exposed.
- Type
(source: Source<unknown>) => number
The direct observers of a native source: its live subscriptions (for a Derived, those whose observation is not deferred) plus the Deriveds that observe it as an input. A plain count; no edge is exposed.
- Type
() => CoordinatorSnapshot
Pending work, queued admissions and the suspended backlog, if any.
- Type
() => void
Resumes a suspended backlog deterministically, as the next boundary would.
Whether an Operation is disposed and how many of its invocations have not ended. Plain counts; no invocation record is exposed.
<TInput, TResult>(operation: Operation<TInput, TResult>) => OperationDescriptionCancels invocation as an unexpected terminal transition would, bypassing the call guards that make that impossible in ordinary use. Called inside the invocation's own commit, it exercises the commit's revocation, which the guards otherwise keep unreachable.
<TResult>(invocation: Invocation<TResult>) => void<TInput = void, TResult = unknown>(options?: ControlledRunOptions<TInput>) => ControlledRun<TInput, TResult>Types
interface InstrumentationEvent {
readonly seq: number;
readonly type: EventType;
readonly resource?: ResourceRef;
readonly scope?: ScopeRef;
readonly cause: Cause;
readonly suppressed?: number;
readonly detail?: EventDetail;
}interface ErrorCapture {
take(): unknown[];
release(): void;
}interface Recorder {
readonly events: readonly InstrumentationEvent[];
ofType(type: EventType): InstrumentationEvent[];
about(resource: object): InstrumentationEvent[];
idOf(resource: object): number;
detach(): void;
}interface ScopeInspection {
readonly state: ScopeState;
readonly members: readonly MemberDescription[];
}interface ControlledRunOptions<TInput> {
readonly prefix?: (input: TInput, ctx: OperationContext) => void;
readonly onAbort?: (id: InvocationId, ctx: OperationContext) => void;
}A run whose invocations are held until the test resolves or rejects each one by its identifier, in any order. It records what started and whether each signal was aborted, and lets the test reach every run's context. Identifiers are per Operation, so one controlled run serves one Operation.
interface ControlledRun<TInput, TResult> {
readonly run: (input: TInput, ctx: OperationContext) => Promise<TResult>;
readonly started: readonly InvocationId[];
input(id: InvocationId): TInput;
context(id: InvocationId): OperationContext;
aborted(id: InvocationId): boolean;
resolve(id: InvocationId, value: TResult): void;
reject(id: InvocationId, reason: unknown): void;
}