State you can address.
A framework-agnostic, type-safe state engine built around structural paths,
targeted subscriptions, and atomic commits.
Documentation
·
Getting started
·
Advanced guide
·
Playground
[!WARNING]
Segment is experimental and currently follows 0.x versioning. Its core behavior
is tested, but public APIs may still change before 1.0.
What is Segment?
Segment is a small state engine for applications where data has a natural address:
records, documents, caches, server payloads, and large keyed collections.
Many state APIs make a selector, atom object, or store snapshot the identity at the
call site. Segment instead gives every declared value a structural path:
users/42/profile/name
That path can be read, written, observed, serialized, or passed to an external
service without sharing an in-memory object reference. The same addressing model
powers fine-grained subscriptions, transaction logs, server hydration, resources,
and adapters.
| Structural paths | Any declared value can be reached through a typed ref or a plain path string. |
| Targeted updates | A write wakes observers of the affected address, not the whole store. |
| Atomic commits | Multiple writes land together; a thrown transaction is rolled back. |
| O(observed) memory | Large segments materialize nodes only for addresses currently being watched. |
| Async state built in | Resources support caching, cancellation, staleness, live data, and save-back. |
| Runtime independent | The core imports neither a UI framework nor the DOM. |
Benchmark
The suite combines exact work counts with a 20,000-record workload, 2,000 targeted
writes, and a 200-row mounted window. Callback, selector, and retained-entry counts
are the primary results; elapsed time and heap measurements are machine-specific
and should be read directionally.
Run it locally with pnpm benchmark. The full
benchmark methodology
documents the fixture, library versions, measurement caveats, and the commands used
to compare a candidate change against an identical baseline. The comparison
describes design trade-offs, not a universal ranking for every application shape.
Installation
npm install segment-state
pnpm add segment-state
Segment is ESM-only. Node.js 22+ is required for Node runtimes and development
tooling. Browser and worker builds do not depend on Node or the DOM.
For the optional Octane adapter, install the peer dependency too:
pnpm add segment-state octane
Quick start
Declare the shape of the state once. Segment turns it into a typed tree of
addresses while keeping values inside the store.
import { createStore, segment } from 'segment-state';
export const store = createStore({
todos: segment({ title: '', completed: false }),
});
export const s = store.state;
s.todos.replaceAll({
docs: { title: 'Ship the documentation', completed: false },
release: { title: 'Publish the package', completed: false },
});
const completed = s.todos.at('docs').completed;
const stop = store.observe(completed, () => {
console.log('completed:', store.get(completed));
});
store.update(completed, (value) => !value, 'todo/toggle');
console.log(completed.path);
stop();
There is no provider and no hidden global store:
createStore() creates one isolated state container.
store.state exposes typed refs; it does not expose mutable state objects.
store.get() reads once, while store.observe() subscribes.
- Every write becomes a named commit that adapters and external services can see.
Atomic updates
Use store.act() when several writes must become visible together:
store.act((tx) => {
tx.set(s.todos.at('docs').completed, true);
tx.set(s.todos.at('release').completed, true);
}, 'release/complete');
Observers see one commit and never an intermediate state. If the callback throws,
none of its writes are published.
Define the state model
Ordinary values are ordinary writable state. Markers are only needed when the
value itself cannot describe the behavior you want.
count: 0 | A writable value with an inferred type. |
profile: { … } | A branch whose fields each receive an address. |
cell<T>(initial) | A narrowed union or a plain object stored as one value. |
segment({ … }) | A large keyed collection with observation-scaled memory. |
list({ … }) | An addressable array whose items have addressable fields. |
derived<T>() | A cached synchronous value computed from other addresses. |
resource<T>() | Async state with load, save, cancellation, staleness, and live data. |
action() | One synchronous, atomic state transition. |
task() | An async flow made of several atomic transitions. |
Computed slots and callable actions receive their implementations through
.with(). A store containing only plain data does not need this step.
import { action, createStore, derived } from 'segment-state';
export const counter = createStore({
count: 0,
doubled: derived<number>(),
increment: action<[by?: number]>(),
}).with((s) => ({
doubled: (get) => get(s.count) * 2,
increment: (tx, by = 1) => tx.update(s.count, (count) => count + by),
}));
counter.state.increment(2);
console.log(counter.get(counter.state.doubled));
See the state model guide for
collections, refs, derivations, actions, and tasks. Resources, SSR, ports, and
persistence boundaries live in the
advanced guide.
Read and write from anywhere
The core API is deliberately small:
store.get(ref) | Read a value without subscribing. |
store.observe(ref, cb) | Subscribe to one address or subtree. |
store.set(ref, value) | Replace one writable value. |
store.update(ref, fn) | Apply one read-modify-write transition. |
store.patch(ref, data) | Update selected fields as one commit. |
store.act(fn) | Group multiple reads and writes atomically. |
store.ref(path) | Resolve an address from a structural path string. |
store.commits(cb) | Subscribe to the serializable stream of state changes. |
This makes the same store usable from UI code, tests, workers, sockets, persistence
layers, and developer tools.
Octane integration
segment-state/octane is the optional render-aware adapter. It has no provider and
keeps subscriptions scoped to the address read by a component.
import { useValue } from 'segment-state/octane';
export function TodoRow({ id }: { id: string }) @{
const [completed, setCompleted] = useValue(s.todos.at(id).completed);
<button onClick={() => setCompleted(!completed)}>
{completed ? 'Done' : 'Mark complete'}
</button>
}
useValue(ref) reads writable, derived, branch, and resource addresses.
useStatus(ref) exposes resource state without suspending.
useDraft(ref) keeps a local edit and publishes it on demand.
The core remains usable without Octane through get, observe, commit streams, and
ports. No React or Vue adapter is bundled today.
Where Segment fits
Segment is a strong fit when:
- a large keyed dataset has a much smaller visible or observed window;
- state must be addressed outside the component that created it;
- several writes must be atomic and attributable;
- server payloads, workers, sockets, or devtools need one serializable protocol;
- async values should share the same address and lifecycle model as local state.
For a small amount of component-local UI state, the state primitive built into your
renderer is usually simpler. Segment is also intentionally not a router, database,
or request client—it coordinates application state around those systems.
Package entry points
segment-state | Framework-agnostic core. |
segment-state/core | Compatibility alias for the same core. |
segment-state/octane | Core plus the optional Octane integration. |
Documentation
| Documentation site | Searchable guide and API concepts. |
| Getting started | Installation, first store, subscriptions, and transactions. |
| State model | Cells, branches, segments, lists, derivations, and actions. |
| Advanced guide | Resources, Octane, SSR, hydration, ports, and guarantees. |
| Core internals | Trie design, commit protocol, complexity, and measurements. |
| Release guide | Maintainer release and trusted publishing workflow. |
Development
pnpm install
pnpm check
Run the interactive example with pnpm playground, or build it with
pnpm playground:build. The playground consumes the package through workspace:*;
pnpm pack:check additionally installs the real tarball in a clean offline consumer
to catch missing files or broken exports.
License
MIT © Michal Makowski