Why Causl
This page is for adopters deciding whether to use causl. It covers what causl exists to solve, how it compares to MobX, Jotai, and Redux Toolkit at high level, and the cases where one of those is a strictly better answer. If you have not read Introduction to Causl yet, read that first; it covers the engine fundamentals this page assumes.
The motivation
The TypeScript reactive-state ecosystem is genuinely good at the problems its libraries were designed for. Redux is excellent at transactional single-store state with time-travel debugging. MobX is excellent at ergonomic observable objects. Jotai is excellent at fine-grained atoms that compose. TanStack Query is the gold standard for server-cache state. Zustand and Valtio prioritise ergonomics and small bundle size. XState is the canonical statechart implementation in the language.
Each one is the right answer for the slice of the problem it owns. None of them is the right answer when the slices overlap. The repeating pattern in real applications — spreadsheets, CMMS, BIM-style asset graphs, capital planning tools, scheduling and Gantt systems, scenario planners, dashboard composers — is that more than one of the following is true at the same time:
- A single user edit cascades through dozens of derived values.
- The set of inputs a derivation depends on changes as the user navigates.
- Async fetches can return after their target dependency has moved.
- Wrong propagation order produces visible-but-inconsistent intermediate frames.
- The bug, if it ships, is data corruption rather than a render glitch.
The honest engineering answer to those problems used to be: stitch Redux's transactional commits, MobX's auto-dependency tracking, TanStack Query's stale protection, and XState's lifecycle modelling together, and pray your team holds the seams in code review forever. Causl is the single library that closes those seams by construction.
How causl is shaped
Causl is held together by fourteen commitments in SPEC §17.1 — the original eight commitments from the engineering shape, plus four §16A race-detection rows, plus the post-wave capability-cost residual (commitment 13, MECHANICAL), plus the host-tier substrate compatibility contract for the opt-in WASM backend (commitment 14, DESIGN-DISCIPLINE). Each commitment is marked MECHANICAL when a CI gate enforces it or DESIGN-DISCIPLINE when the discipline is enforced by review policy. Kent Beck's framing of commitments-as-contract is the load-bearing argument the SPEC names; Robert Martin and Martin Fowler are cited as counter-checks on the MECHANICAL / DESIGN-DISCIPLINE distinction.
The eight engineering commitments — denotational semantic foundation,
transactions as the only mutation boundary, deterministic dynamic-dep
cleanup, a single composite statechart, strict model/controller/engine
layering, discriminated-union state everywhere, MVU-shaped application
surface, and pre-runtime race detection in CI — are the shape the API is
allowed to grow into. Every public surface addition has to name an
unavoidable engine concept that the existing surface cannot express, or
it doesn't ship. Today that means a canonical seven-method
Graph plus a curated second tier of nine extensions; SPEC
§12.2 audits the surface quarterly.
How causl compares — at a high level
This is the short version of the comparison table on the home page. Each library is the strictly better answer for the problem it owns; the rows below explain why causl's shape is different rather than just bigger.
vs Redux Toolkit
Redux Toolkit's strength is the transactional store plus the time-travel
devtools that fall out of it. Every action is an atomic write, every
action is logged, and the log is replayable. Causl shares that
architectural commitment — graph.commit(intent, tx => …)
is the only mutation boundary, and commitLog is the
replayable log — but adds three things RTK does not:
-
Automatic dependency tracking. RTK selectors are
hand-written; you remember to call
createSelectorwith the right input slices and to memoise. Causl'sgraph.derivedtracks reads through thegethandle, so a derivation that today readsassetAand tomorrow readsassetBis correct by construction. -
Glitch-freedom as a theorem. Diamond derivations in
RTK can produce intermediate values if you forget to compose selectors
correctly. In causl, glitch-freedom is the denotational consequence of
Behavior a = GraphTime → a; a derived value at time t is a pure function of its inputs at the same t. There is no intermediate time, therefore no intermediate value. -
Pre-runtime race detection. The
causl-checkbinary (twelve passes against the IR) and the boundedcausl-enumeratebinary catch cycles, subscribe-without-dispose, cross-graph reads, glitch propagation, and the other §9.1 STATIC and MODEL race classes before the code reaches production.
Where RTK is the right choice: a flat single store, a small selector
graph, a team that wants a well-trodden path with broad community
tooling. RTK Query is also the right choice for HTTP cache state
specifically; @causl/sync overlaps but is not a drop-in
replacement.
vs MobX
MobX is the closest peer in ergonomics — both libraries do automatic dependency tracking, both libraries support derived values. The shape of disagreement is at the commit boundary:
-
Atomic commit boundary. MobX
runInActiongroups reactions, but the engine is not transactional — a throw insiderunInActiondoes not roll back the writes already executed. Causl'sgraph.commitis atomic-or-nothing; if the callback throws, no writes land and no subscribers fire. - Glitch-freedom as a theorem, not best-effort. MobX ships a scheduler that is glitch-free in practice; the SPEC for MobX does not name glitch-freedom as a contract. Causl names it denotationally and verifies it with 1000-trial property tests at the §15.2 floor.
-
Replay determinism as a contract. Causl pays a
measurable
1.84× engine baseline × 3.5× replay-determinism contract premium(a 6.4× residual) versus MobX on contract-bearing benchmark cells. That cost is named in SPEC §17.5 (commitment 13) and gated by a CI invariant —mobx_median × 3.0 ≤ causl_median ≤ mobx_median × 8.0onequality-cutoff × 10000,equality-cutoff-fanout-10k × 10000, andspreadsheet-100x100 × 10000. Causl buys four capabilities MobX does not ship:commitLogrebuild on every commit,changedNodesset construction for subscribers,GraphTimemonotonicity stamping, andreadAt/snapshotAtretention bookkeeping.
Where MobX is the right choice: an existing MobX codebase you want to improve incrementally, or an application whose state is genuinely object-graph-shaped with no transactional cross-field invariants.
vs Jotai (and Recoil)
Jotai is excellent for fine-grained atoms that compose without ceremony.
Atoms support derivations (read-only atoms with a get
function), and the dependency graph is built automatically. Where causl
differs:
- Cross-atom transactions. Jotai does not have a transaction boundary. Writing to two atoms produces two re-renders; subscribers that read both atoms can observe an intermediate state. Causl's commit produces exactly one notification per subscriber per transaction regardless of how many inputs were written.
-
Dynamic-dep cleanup proven correct. Both libraries
clean up dependencies that go unread, but causl's dynamic-dep cleanup
is held by a 1000-trial property suite (§9.1 row, §15.2 floor) and is
catchable by
causl-checkat lint time. -
Conflict records as data. Causl treats async-fetch
conflicts and version-stamp mismatches as first-class queryable state
—
@causl/syncregisters them in the conflict registry, not as exceptions thrown out of band. Jotai pushes conflict handling into the application layer.
Where Jotai is the right choice: small global state where atomicity does not matter, applications where the atomic-write boundary is cell-by-cell (e.g. an input form), or codebases that already lean into Suspense for everything async.
vs everything else
Zustand and Valtio prioritise
ergonomics and bundle size; neither addresses dependency cascades,
conflicts, or async safety as first-class concerns. Reach for them when
you have a flat store and a small surface.
TanStack Query is the right answer for HTTP cache state
specifically; @causl/sync is complementary, not a
replacement. XState is the closest peer in spirit for
lifecycle modelling — causl uses statecharts internally (one composite
chart for every lifecycle, per SPEC §6) but adds the dependency engine
on top. Reach for XState directly when your problem is "a wizard with
five steps and a back button," not "a dependency graph with statecharts
governing the async edges."
When to use causl
Reach for causl when two or more of these are true:
- Your state is a graph of facts where one user action cascades through dozens of derived values.
- Your derivations change what they depend on as the user navigates.
- You have async fetches whose results may be stale by the time they return.
- You need a typed audit trail of every state change with named intents.
- You need conflict records that survive the transaction that created them — not exceptions, data.
- You have spreadsheet-like cells with formula references, or asset hierarchies with reference-based dependencies.
- You want to catch race conditions in CI before they reach production.
- A bug in your state propagation is data corruption, not a UI glitch.
When not to use causl
Reach for something else when:
- Your state is a flat object with maybe twenty fields and no cross-field derivations — use Zustand or Jotai.
- Your state is mostly cached HTTP responses — use TanStack Query, or Apollo / Relay if GraphQL.
- Your problem is "one giant form with validation" — use React Hook Form.
- Your problem is "a wizard with five steps and a back button" — use XState directly.
- You want a library you can adopt incrementally without thinking about your model layer. Causl asks you to commit to a layered approach (information model vs editor controllers vs engine substrate). That is a feature for the problems above and overhead for the problems below.
Honest limit — state that is a tree of values. Causl is not the right tool for state that is genuinely tree-shaped. The denotational machinery, the commit pipeline, the IR, the CI gates, and the cost of laying out information-model / controller / engine in separate packages are all overhead a flat store does not need. Causl is over-engineered for simple apps and the only way to ship the complex ones without losing your mind. Pick the right tool — the SPEC is explicit about this at §18 and the README repeats it as a closing summary.
What causl is not
This list is repeated from the repo README because the previous SPEC draft promised too much:
- Not a spreadsheet engine.
@causl/formulademonstrates spreadsheet patterns on top of the core. It does not ship VLOOKUP. - Not a CRDT. Multi-user merge semantics belong in a layer above this one.
- Not a database, message bus, workflow engine, or rules engine.
- Not a competitor to Redux/MobX/Jotai/XState for problems they already handle well.
- Not yet at 1.0. Phases 1–4 ship on
main; APIs are stable but not version-locked. The pure-TypeScript engine is the open-source floor you install from@causl/core. A production Rust→WebAssembly engine also exists and is the Enterprise path (viacausl-client, where rust-ssot is the unconditional default since 2026-06-21) — a real Rust engine, not a TS wrapper.
Where to go next
Once you have decided causl fits, the
SPEC
is the normative source of truth and
SPEC.async
covers the async-resource statechart. The
@causl/core API reference
is generated from source on every build. The migration rule catalogue at
docs/migration/RULE_CATALOGUE.md
maps common Jotai / MobX / Redux patterns to their causl equivalents and
pairs each rule with a @causl/migration-check codemod.