Design Philosophy
StateLoom is a universal state management SDK that unifies the best patterns from the JavaScript state management ecosystem into a single, coherent library. This document explains the core design principles and the reasoning behind every major architectural decision.
Core Thesis
The signal graph is the universal primitive for state management. Every paradigm — stores, atoms, proxies — can be expressed as compositions of signals and computed values. By building on a signal-based reactive core, StateLoom achieves paradigm unification without paradigm compromise.
Design Principles
1. Zero-Barrier Adoption
Developers should be productive within minutes, not hours. StateLoom achieves this by:
- Familiar APIs: Each paradigm mirrors the API patterns developers already know (Zustand's
createStore, Jotai'satom, Valtio'sobservable). - Incremental adoption: Start with any single paradigm. Add others as needed. The core is always the same.
- No boilerplate: Creating a store is a single function call. No providers required for basic usage. No action types, reducers, or dispatchers.
- TypeScript-first with inference: Types flow automatically. Developers rarely need to annotate.
2. Minimal Core, Additive Complexity
The core reactive kernel targets ~1.5 KB gzipped. Every feature beyond basic reactivity is a separate, tree-shakeable package:
| What you need | What you install | Approximate size |
|---|---|---|
| Reactive primitives only | @stateloom/core | ~1.5 KB |
| Store pattern | + @stateloom/store | +~0.5 KB |
| React bindings | + @stateloom/react | +~0.3 KB |
| Persistence | + @stateloom/persist | +~1.0 KB |
| DevTools | + @stateloom/devtools | +~0.8 KB |
You pay only for what you use. A React app using the store paradigm with persistence loads roughly 3.3 KB gzipped — competitive with Zustand alone.
3. Framework-Agnostic by Architecture, Not by Afterthought
The entire SDK is designed around a universal Subscribable<T> contract:
interface Subscribable<T> {
get(): T;
subscribe(callback: (value: T) => void): () => void;
}Every signal, computed, store, and atom implements this interface. Framework adapters are thin bridges (50–200 lines each) that connect this contract to framework-specific reactivity:
- React:
useSyncExternalStore(signal.subscribe, signal.get) - Vue:
shallowRefwrapper withonScopeDisposecleanup - Svelte: Native store contract — signals work with
$storesyntax directly - Solid:
createSignalbridge - Angular:
Observablewrapper vianew Observable(subscriber => signal.subscribe(...))
This means the core is genuinely runtime-agnostic: browser, Node.js, Bun, Deno, Web Workers, Service Workers.
4. SSR as a First-Class Primitive
Most state libraries treat SSR (Server-Side Rendering) as an afterthought, leading to the "store singleton in serverless" bug class. StateLoom makes SSR isolation a core primitive via the Scope model, inspired by Effector's Fork API:
createScope()creates an isolated state universerunInScope(scope, fn)executes operations within that scopeserializeScope(scope)extracts state for client hydration
Every server request gets its own scope. No shared mutable state between requests. No data leakage. This is enforced by design, not convention.
5. Push-Pull Hybrid Reactivity
The reactive core uses the push-pull hybrid algorithm proven by Preact Signals and Angular Signals:
- Push phase: When a signal changes, "dirty" marks propagate eagerly through the dependency graph
- Pull phase: Computed values only recompute when actually read (lazy evaluation)
This solves the diamond problem without glitches: if node D depends on B and C (both depending on A), D never sees an intermediate state where only B has updated. The push phase marks everything dirty; the pull phase recomputes in the correct order.
6. Composition Over Configuration
Following Zustand's proven middleware pattern, StateLoom uses middleware composition for cross-cutting concerns:
const store = createStore((set, get) => ({ count: 0 }), {
middleware: [logger(), devtools({ name: 'Counter' }), persist({ key: 'counter' })],
});Middleware wraps state operations in a pipeline. Each middleware intercepts set, get, subscribe, and lifecycle events. The type system tracks middleware mutations through the chain, preserving full type safety.
7. TC39 Signals Alignment
The core API aligns with the TC39 Signals proposal (currently Stage 1). When native signals ship in JavaScript engines, StateLoom can delegate to them with zero API changes. This makes the SDK architecturally future-proof — developers invest in learning an API surface that will become a platform primitive.
What StateLoom Is Not
- Not a state machine library: Use XState for complex statecharts. StateLoom handles reactive data flow.
- Not a server state cache: Use TanStack Query for server state synchronization. StateLoom handles client-side and isomorphic state.
- Not a replacement for framework-native state: For simple component-local state, use
useState/ref/createSignal. StateLoom is for shared, cross-component, and cross-boundary state.
Principle-to-Implementation Mapping
Each design principle maps to concrete implementation decisions:
Design Trade-off Decisions
Key architectural trade-offs and the rationale behind each choice:
| Trade-off | Choice | Alternative Considered | Rationale |
|---|---|---|---|
| Push vs pull reactivity | Push-pull hybrid | Pure push (MobX), Pure pull (polling) | Avoids glitches and unnecessary computation |
| Signal equality | Object.is default | Deep equality, === only | Handles NaN, avoids deep comparison cost |
| Middleware attachment | Array on createStore | Global plugin registry | Explicit composition, no hidden global state |
| Atom state location | Config object + Scope | Value in atom instance | SSR isolation without recreating atoms |
| Proxy depth | Lazy deep proxying | Eager proxying at creation | Avoids proxying unused subtrees |
| Effect scheduling | Synchronous (batched) | Always async (microtask) | Predictable ordering, no stale reads |
| Bundle strategy | One package per concern | Monolithic bundle | Tree-shaking, pay-for-what-you-use |
| Adapter pattern | Per-framework package | Universal HOC/wrapper | Native feel per framework, smaller adapters |
Design Influences
| Library | What StateLoom borrows | What StateLoom improves |
|---|---|---|
| Preact Signals | Push-pull hybrid reactivity, doubly-linked dependency graph | Adds paradigm adapters, middleware, SSR |
| Zustand | Middleware composition, minimal API surface | Adds signal core, multi-paradigm, SSR isolation |
| Jotai | Bottom-up atom composition, Suspense integration | Unified with store/proxy paradigms via shared core |
| Valtio | Proxy-based mutable syntax, structural sharing snapshots | Shared reactive core instead of separate implementation |
| Effector | Fork API for SSR isolation, per-request scoping | Simplified API, smaller bundle |
| Nanostores | Minimal footprint, framework adapter pattern | Multi-paradigm support, middleware system |
| Legend State | Declarative persistence with plugin-based sync | Integrated into middleware pipeline |
| TanStack Store | Universal Subscribable contract for adapters | Full paradigm adapters, not just raw signals |
Cross-References
- Architecture Overview — system structure, dependency graph, build order
- Core Design — push-pull algorithm, dependency graph details
- Layer Scoping — package boundaries enforcing minimal core principle
- Middleware Overview — composition-over-configuration in practice
- Adapters Overview — framework-agnostic bridging