State management as graph topology, not tree hierarchy.
Installation • Quick Start • Comparison • Packages
Real application state is a graph: user data affects cart, cart affects checkout, checkout affects orders. Most libraries force that graph into a tree — so you end up with implicit dependencies, mystery re-renders, and cycles you only discover at runtime.
TopoJS models state as what it actually is. You declare nodes, the edges between them, and the kind of relationship each edge represents. The runtime computes update order, detects cycles at creation time, and propagates changes automatically.
import { statespace, node, derives, requires, influencedBy } from '@topojs/core';
const Cart = statespace('Cart', {
nodes: {
user: node({ initial: { membership: 'free', authenticated: false } }),
items: node({ initial: [] }),
discount: node({ initial: 0 }),
total: node({ initial: 0 }),
canCheckout: node({ initial: false }),
},
topology: {
discount: derives(['user.membership'], (m) => (m === 'premium' ? 0.2 : 0)),
total: derives(['items', 'discount'], (items, d) => sum(items) * (1 - d)),
canCheckout: requires(['total > 0', 'user.authenticated']),
'user.recommendations': influencedBy(['items']),
},
constraints: { noCyclesThrough: ['total'] },
});What they share: reactive primitives, derived values, React integration via useSyncExternalStore.
What TopoJS adds:
- Named edge types — instead of "atom depends on atom", you say how:
derives(computed value),requires(boolean gate),influencedBy(eventual consistency),triggers(side effect). The relationship is first-class, not implicit. - Cycle detection at creation time — Jotai and Recoil have no cycle detection; infinite loops surface at runtime. TopoJS runs a DFS when the statespace is created and throws before any user interaction.
noCyclesThroughconstraint — you can allow cycles in low-risk parts of the graph while strictly guarding critical nodes likecheckoutororders.- Graph introspection —
space.dependsOn(path),space.affects(path),space.updateOrder(path)are queryable at runtime. Atoms have no structural API.
What they share: a single source of truth, subscription model, TypeScript support.
What TopoJS adds:
- Automatic propagation — you don't write reducers or selectors manually. Declare the topology once; the runtime handles update order and derived values.
- Async derivations as first-class edges —
derivesaccepts aPromise-returning function withloadinganderroroptions. No middleware, no thunks. - Structural tooling — the
topoCLI can analyze, validate, trace, and visualize any statespace without touching the app.
What they share: automatic reactivity, computed values, subscriptions without manual wiring.
What TopoJS adds:
- Explicit, typed edges — MobX reactivity is implicit and scattered across class decorators. In TopoJS, all relationships live in one
topologyrecord that you can read, export, and analyze. - Constraint system — MobX has no concept of
noCyclesThroughormaxFanout. TopoJS constraints are part of the statespace definition and enforced at runtime.
npm install @topojs/core @topojs/react// store.ts
import { statespace, node, derives, requires } from '@topojs/core';
export const CartSpace = statespace('Cart', {
nodes: {
items: node({ initial: [] as number[] }),
discount: node({ initial: 0 }),
total: node({ initial: 0 }),
canCheckout: node({ initial: false }),
},
topology: {
total: derives(
['items', 'discount'],
(items, d) => (items as number[]).reduce((a, b) => a + b, 0) * (1 - (d as number)),
),
canCheckout: requires(['total > 0']),
},
});
// Cart.tsx
import { useNode, useMutation } from '@topojs/react';
import { CartSpace } from './store';
export function Cart() {
const total = useNode<number>(CartSpace, 'total');
const canCheckout = useNode<boolean>(CartSpace, 'canCheckout');
const { append } = useMutation(CartSpace, 'items');
return (
<div>
<p>Total: {total}</p>
<button disabled={!canCheckout}>Checkout</button>
<button onClick={() => append(10)}>Add item ($10)</button>
</div>
);
}| Package | npm | Description |
|---|---|---|
packages/core |
@topojs/core |
Runtime engine — statespace, nodes, edges, cycle detection, propagation |
packages/react |
@topojs/react |
React hooks — useNode, useNodes, useMutation, useTopology |
packages/cli |
@topojs/cli |
CLI — analyze, visualize, check, optimize, export, trace |
packages/vite-plugin |
@topojs/vite |
Vite plugin with /topo visualizer middleware |
Each package has its own README with full API documentation.
npx @topojs/cli analyze dist/store.js # print nodes and topology
npx @topojs/cli check dist/store.js # validate constraints
npx @topojs/cli trace dist/store.js total # trace propagation for a node
npx @topojs/cli visualize dist/store.js # open interactive graph in browser
npx @topojs/cli export dist/store.js --format mermaidgit clone https://github.com/creativoma/topojs
cd topojs
pnpm install
pnpm testSee CONTRIBUTING.md.
MIT © 2026 TopoJS Contributors
State is a graph. Treat it like one.