@uistate/core 5.5.2
Path-based state management with wildcard subscriptions and async support. ~6 KB, zero dependencies.
Install
npm i @uistate/core
Quick Start
import { createEventState } from '@uistate/core';
const store = createEventState({ count: 0, user: { name: 'Alice' } });
// Subscribe to a path
const unsub = store.subscribe('count', (value) => {
console.log('Count:', value);
});
// Update state
store.set('count', 1); // logs "Count: 1"
store.get('count'); // 1
// Cleanup
unsub();
createEventState(initialState)
Creates a new reactive store. The initial state is deep-cloned.
const store = createEventState({ count: 0 });
const empty = createEventState(); // empty store, add paths later
store.get(path?)
Retrieve a value by dot-separated path. Returns the entire state if no path is provided. Returns undefined for non-existent paths (no errors).
store.get('user.name'); // 'Alice'
store.get('user'); // { name: 'Alice' }
store.get(); // entire state object
store.get('no.such.path'); // undefined
store.set(path, value)
Set a value at a path and notify all matching subscribers. Creates intermediate objects if they don't exist. Returns the value that was set.
store.set('count', 42);
store.set('user.email', 'alice@example.com'); // creates 'email' key on user
store.set('deep.nested.path', true); // creates deep, deep.nested
Notification order
When you call store.set('user.name', 'Bob'), subscribers fire in this order:
- Exact — subscribers on
'user.name' - Wildcard — subscribers on
'user.*' - Global — subscribers on
'*'
store.subscribe(path, handler)
Subscribe to changes at a path. Returns a function that removes the subscription.
Exact path subscribers
Handler receives (value, detail) where detail is { path, value, oldValue }.
store.subscribe('count', (value, { oldValue }) => {
console.log(`${oldValue} → ${value}`);
});
Wildcard subscribers
Handler receives (detail) where detail is { path, value, oldValue }.
store.subscribe('user.*', ({ path, value }) => {
console.log(`${path} changed to`, value);
});
// Fires when any direct child of 'user' changes:
store.set('user.name', 'Bob'); // fires
store.set('user.email', 'x'); // fires
store.set('count', 1); // does NOT fire
Global subscribers
store.subscribe('*', ({ path, value }) => {
console.log(`[state] ${path} =`, value);
});
// Fires on every set() call — useful for logging/telemetry
store.setAsync(path, fetcher)
Run an async operation with automatic status tracking. Manages three sub-paths:
| Sub-path | Values |
|---|---|
${path}.status | 'loading' → 'success' | 'error' | 'cancelled' |
${path}.data | The resolved value |
${path}.error | Error message string, or null |
await store.setAsync('users', async (signal) => {
const res = await fetch('/api/users', { signal });
return res.json();
});
store.get('users.status'); // 'success'
store.get('users.data'); // [...]
If setAsync is called again on the same path while a previous call is still in-flight, the previous call is automatically aborted.
store.cancel(path)
Abort an in-flight setAsync operation. Sets status to 'cancelled'.
store.setAsync('users', fetcher); // starts loading
store.cancel('users'); // aborts, status → 'cancelled'
store.destroy()
Tear down the store. Aborts all in-flight async operations, clears all subscriptions. Any further calls to get, set, or subscribe will throw.
Query Client
A convenience wrapper around setAsync for data-fetching patterns. Uses the query.${key} namespace internally.
import { createEventState } from '@uistate/core';
import { createQueryClient } from '@uistate/core/query';
const store = createEventState();
const qc = createQueryClient(store);
qc.query(key, fetcher)
await qc.query('users', async (signal) => {
const res = await fetch('/api/users', { signal });
return res.json();
});
qc.subscribe(key, cb) / subscribeToStatus / subscribeToError
qc.subscribe('users', (data) => renderUsers(data));
qc.subscribeToStatus('users', (status) => showSpinner(status === 'loading'));
qc.subscribeToError('users', (err) => showError(err));
qc.getData / getStatus / getError
qc.getData('users'); // [...] or undefined
qc.getStatus('users'); // 'success' | 'loading' | 'error' | 'idle'
qc.getError('users'); // null or error message
qc.cancel(key) / qc.invalidate(key)
qc.cancel('users'); // abort in-flight request
qc.invalidate('users'); // reset data, status, error to null/idle
Wildcard Patterns
When store.set('user.profile.name', 'Bob') is called, these subscribers fire:
| Subscription | Fires? | Handler args |
|---|---|---|
'user.profile.name' | Yes | (value, detail) |
'user.profile.*' | Yes | (detail) |
'user.*' | Yes | (detail) |
'*' | Yes | (detail) |
'user.profile' | No | — |
'user' | No | — |
Wildcards match on parent paths, not the exact path being set. This mirrors how DOM event bubbling works.
Async Patterns
Loading indicator
store.subscribe('users.status', (status) => {
spinner.hidden = status !== 'loading';
});
store.setAsync('users', fetchUsers);
Race condition handling
// User types fast — each keystroke triggers a search
input.addEventListener('input', () => {
// setAsync auto-aborts the previous in-flight request
store.setAsync('search', (signal) =>
fetch(`/api/search?q=${input.value}`, { signal }).then(r => r.json())
);
});
Error handling
try {
await store.setAsync('users', fetchUsers);
} catch (err) {
if (err.name === 'AbortError') return; // cancelled, ignore
console.error('Fetch failed:', err);
}
// Or subscribe to the error path:
store.subscribe('users.error', (msg) => {
if (msg) showToast(`Error: ${msg}`);
});