@morphixai/store
v0.0.8
Published
Minimal observable store with React bindings — SSR-safe, reentrancy-guarded.
Maintainers
Readme
@morphixai/store
Class-based reactive store. No framework dependency, no proxy magic — a plain
Store<T> subclass with get / set / patch / subscribe, a built-in
computed cache, selector-scoped subscriptions, and an opt-in React adapter at
@morphixai/store/react.
When to use
- Cross-platform state (web + native + node + workers + CLI) where MobX or Zustand bring more surface and runtime weight than you want.
- Domain stores that want explicit, debuggable mutation paths (
set/patch) rather than auto-tracked proxies. - Layers reused outside React — Vue/Svelte/Solid bridges, background jobs, tests.
Install
npm install @morphixai/storeQuick start
import { Store } from '@morphixai/store';
interface CounterState {
count: number;
doubled: number; // computed
}
class CounterStore extends Store<CounterState> {
constructor() {
super({ count: 0, doubled: 0 });
this.computed('doubled', (s) => s.count * 2);
}
increment() {
this.set('count', this.state.count + 1);
}
}
const counter = new CounterStore();
counter.subscribe((next, prev) => console.log(prev.count, '→', next.count));
counter.increment(); // logs: 0 → 1
counter.doubled; // 2 — computed values live on the instance
counter.state.count; // 1 — plain values live on state
computed(key, fn)defines a getter on the store instance, not insidestate. Read it ascounter.doubled;counter.state.doubledreturns whatever was passed tosuper()and never updates.
Selector subscriptions
Only fires when the selected slice changes:
const off = counter.subscribe(
(s) => s.count,
(next, prev) => console.log('count', prev, '→', next),
);
off(); // unsubscribeSelectors read from state, so they select plain fields — selecting a computed
name would read the stale state copy and never fire.
React
import { useStore } from '@morphixai/store/react';
function Counter() {
const count = useStore(counter, (s) => s.count); // selector — re-renders on count only
return <button onClick={() => counter.increment()}>{count}</button>;
}React is an optional peer dependency — install it only if you use this entry.
useStore(store) without a selector subscribes to the whole state.
Semantics
stateis deeply readonly. Writes go throughset(key, value)orpatch({ ... })— there is no proxy interception, so a missedsetwill not notify subscribers. That is the trade: explicit writes, no hidden tracking.- The internal state object is replaced on every change, so consumers can
compare with
Object.isto detect changes. This is whatuseSyncExternalStorerelies on, and it is why the React binding is SSR-safe. - Computed values are cached on the instance and invalidated on every notify.
- Reentrant writes from inside a subscriber are guarded — a listener that calls
setwill not corrupt the notification pass.
Subpath exports
| Entry | Contents |
|---|---|
| @morphixai/store | Store, types |
| @morphixai/store/react | useStore |
License
MIT
