Event-based state management for React, powered by RxJS. Dantian exposes a small event store API and React hooks that subscribe to specific property paths, so only the parts of UI that care about a given event re-render.
npm install dantian| Runtime | Supported versions |
|---|---|
| React | 18, 19 |
| RxJS | 7, 8 |
| Node | 18, 20, 22 |
// store.ts
import { createEventStore } from 'dantian';
export const store = createEventStore({
count: 0,
user: { name: 'n/a' },
});
export const {
useStoreValue,
publish,
useHydrateStore,
useIsHydrated,
reset,
feed,
destroy,
state$,
systemEvents$,
} = store;// Counter.tsx
import { useStoreValue } from './store';
export function Counter() {
const [count, setCount] = useStoreValue('count');
return (
<button type="button" onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}At a glance, createEventStore maintains a stream of events and derives state from them. Updates are published by property path (for example, user.name), and hooks subscribe to those paths.
const [name, setName] = useStoreValue('user.name');
setName('Ada');
// Or outside React:
publish('user.name', 'Ada');const store = createEventStore(
{ user: { name: 'n/a' } },
{
hydrator: async () => {
const response = await fetch('/api/profile');
return (await response.json()) as { user: { name: string } };
},
},
);const store = createEventStore(
{ count: 0 },
{
persist: async (state) => {
localStorage.setItem('dantianState', JSON.stringify(state));
},
},
);If hydration or persistence fails, you can observe the error via system events while console errors remain intact:
systemEvents$.subscribe((event) => {
if (event.type === '@@HYDRATE_ERROR' || event.type === '@@PERSIST_ERROR') {
console.error('store error', event.payload.error);
}
});If updates are coming from multiple sources, you can disable local caching per hook:
const [value, setValue] = useStoreValue('profile.name', {
disableCache: true,
});You can also throttle update propagation with throttle (milliseconds):
const [user] = useStoreValue('user', { throttle: 50 });reset({ count: 0, user: { name: 'n/a' } });
feed({ count: 5, user: { name: 'Julius' } });const subscription = state$.subscribe((state) => {
console.log('state changed', state);
});
subscription.unsubscribe();If a store is no longer needed, dispose of it to complete internal subjects and prevent lingering subscriptions:
store.destroy();After destroy(), calls to publish, reset, feed, and the callback from
useHydrateStore() are no-ops.
import { createEventStore } from 'dantian';
const store = createEventStore({ count: 0 }, { debug: false });const [count, setCount] = store.useStoreValue('count');
setCount(count + 1);
// Or publish directly
store.publish('count', 42);useStoreValue options:
disableCache: bypasses local caching to avoid flicker in edge cases.throttle: throttles updates in milliseconds.throtle: legacy alias forthrottle(kept for backward compatibility).
const store = createEventStore(
{ count: 0 },
{
hydrator: async () => ({ count: 10 }),
persist: async (state) => {
localStorage.setItem('dantianState', JSON.stringify(state));
},
},
);In React, you can trigger hydration manually:
const hydrate = store.useHydrateStore();
const isHydrated = store.useIsHydrated();store.reset({ count: 0 });
store.feed({ count: 5 });store.destroy();const sub = store.getPropertyObservable('count').subscribe((value) => {
console.log('count changed', value);
});
sub.unsubscribe();If you need a minimal store with selectors and updates, use buildClassicStore:
import { buildClassicStore } from 'dantian';
const classic = await buildClassicStore({ count: 0 });
const [state, update] = classic.useStore();
const count = classic.useSelector((s) => s.count);
update((prev) => ({ ...prev, count: prev.count + 1 }));With hydration and persistence:
const classic = await buildClassicStore({
beforeLoadState: { count: 0 },
hydrator: async () => ({ count: 2 }),
persist: async (state) => {
localStorage.setItem('classicState', JSON.stringify(state));
},
});Options:
debug?: boolean— log events and state transitions.hydrator?: () => Promise<T>— async hydration source.persist?: (state: T) => Promise<void>— persistence callback.
Returns:
useStoreValue<K>(path, options?): React hook for reading/updating a property path.publish(path, payload): publish an event for a property path.getPropertyObservable(path, throttle?): RxJS observable for a property path.useHydrateStore(): returns a function to emit@@HYDRATEDwith payload.useIsHydrated(): returns a boolean hydration flag.reset(payload): emit@@RESETsystem event.feed(payload): emit@@FEEDsystem event.state$:BehaviorSubject<T>with current state.globalEventStore$:BehaviorSubjectof all events.systemEvents$: observable of system events (event types starting with@@, including@@HYDRATE_ERRORand@@PERSIST_ERROR).destroy(): dispose the store, completing internal subjects and stopping further publishes.
useStoreValue options:
disableCache?: booleanthrottle?: numberthrotle?: number(legacy alias)
defaultState can be either a plain initial state or an object with:
beforeLoadState: Thydrator: () => Promise<T>persist?: (state: T) => Promise<void>
Returns:
useStore(): React hook for[state, update].useSelector(selector): React hook for derived values.update(updater): update function.getValue(): current value getter.subject$:BehaviorSubject<T>.defaultState: the provided default state.
Alias of createEventStore.
- Hydration errors:
systemEvents$emits@@HYDRATE_ERRORand the console logsFailed to hydrate store. Verify the hydrator resolves with the same shape as the initial state and handle thrown errors. - Persist errors:
systemEvents$emits@@PERSIST_ERRORand the console logsFailed to persist store. Ensure your persist callback returns a promise and handles storage quotas or serialization failures. - Disposed stores still referenced: call
destroy()when a store is no longer used, and avoid callingpublish/reset/feedafterward.
- No breaking changes are required.
destroy()is now available to explicitly dispose of stores.systemEvents$now includes@@HYDRATE_ERRORand@@PERSIST_ERRORevents.
MIT