Zero-dependency reactive state management for any framework.
npm install @oxog/state
yarn add @oxog/state
pnpm add @oxog/state
import { createStore, useStore } from '@oxog/state';
// Create a store with state and actions
const store = createStore({
count: 0,
$increment: (state) => ({ count: state.count + 1 }),
$decrement: (state) => ({ count: state.count - 1 }),
});
// Use in React
function Counter() {
const count = useStore(store, (s) => s.count);
const increment = useStore(store, (s) => s.increment);
return <button onClick={increment}>{count}</button>;
}
// Use in vanilla JS
store.subscribe((state) => console.log(state.count));
store.getState().increment();
createStore(initialState)Creates a reactive store.
const store = createStore({
count: 0,
$increment: (state) => ({ count: state.count + 1 }),
});
batch(fn)Batch multiple updates into a single notification.
import { batch } from '@oxog/state';
batch(() => {
store.setState({ count: 1 });
store.setState({ name: 'John' });
}); // Single notification
| Method | Description |
|---|---|
getState() |
Get current state |
setState(partial) |
Update state with partial object |
merge(partial) |
Deep merge state |
reset() |
Reset to initial state |
subscribe(listener, selector?) |
Subscribe to changes |
use(plugin, options?) |
Add plugin |
destroy() |
Cleanup store |
useStore(store, selector?, equalityFn?)Subscribe to store state in React components.
// Full state
const state = useStore(store);
// With selector
const count = useStore(store, (s) => s.count);
// With custom equality
const user = useStore(store, (s) => s.user, deepEqual);
useShallow(selector) v1.2Wrap selectors returning objects to use shallow equality comparison.
import { useStore, useShallow } from '@oxog/state';
// Without useShallow - re-renders on any state change
// const data = useStore(store, s => ({ a: s.a, b: s.b }));
// With useShallow - only re-renders when values change
const data = useStore(store, useShallow(s => ({ a: s.a, b: s.b })));
useStoreSelector(store, selectors) v1.2Select multiple values with independent subscriptions.
import { useStoreSelector } from '@oxog/state';
const { userCount, totalRevenue } = useStoreSelector(store, {
userCount: s => s.users.length,
totalRevenue: s => s.orders.reduce((sum, o) => sum + o.total, 0),
});
useStoreActions(store, ...actionNames) v1.2Get multiple actions with stable references.
import { useStoreActions } from '@oxog/state';
const { increment, decrement, reset } = useStoreActions(
store,
'increment', 'decrement', 'reset'
);
useSetState(store) v1.2Get a stable setState function for partial updates.
import { useSetState } from '@oxog/state';
function Form() {
const setState = useSetState(store);
return (
<input onChange={(e) => setState({ name: e.target.value })} />
);
}
useTransientSubscribe(store, selector, callback) v1.2Subscribe to state changes without causing re-renders.
import { useTransientSubscribe } from '@oxog/state';
function Analytics() {
useTransientSubscribe(
store,
s => s.pageViews,
(views) => analytics.track('page_view', { count: views })
);
return <div>Tracking active</div>;
}
useCreateStore(initialState)Create a store scoped to component lifecycle.
function MyComponent() {
const store = useCreateStore({ count: 0 });
// Store is destroyed when component unmounts
}
useAction(store, actionName)Get a stable action reference.
const increment = useAction(store, 'increment');
Organize state into modular, reusable slices.
import { createStore, createSlice } from '@oxog/state';
const userSlice = createSlice('user', {
name: '',
email: '',
$login: (state, name: string, email: string) => ({ name, email }),
$logout: () => ({ name: '', email: '' }),
});
const cartSlice = createSlice('cart', {
items: [],
$addItem: (state, item) => ({ items: [...state.items, item] }),
});
const store = createStore({
...userSlice,
...cartSlice,
});
// Access: store.getState().user.name
// Action: store.getState().user.login('John', '[email protected]')
Derive values from state with automatic memoization.
import { createStore, computed } from '@oxog/state';
const store = createStore({
items: [{ price: 10, qty: 2 }, { price: 20, qty: 1 }],
totalItems: computed((state) =>
state.items.reduce((sum, i) => sum + i.qty, 0)
),
totalPrice: computed((state) =>
state.items.reduce((sum, i) => sum + i.price * i.qty, 0)
),
});
console.log(store.getState().totalItems); // 3
console.log(store.getState().totalPrice); // 40
Combine multiple stores into a unified interface.
import { createStore, createFederation } from '@oxog/state';
const userStore = createStore({ name: 'John' });
const cartStore = createStore({ items: [] });
const settingsStore = createStore({ theme: 'dark' });
const federation = createFederation({
user: userStore,
cart: cartStore,
settings: settingsStore,
});
// Access all stores
const state = federation.getState();
console.log(state.user.name, state.cart.items, state.settings.theme);
// Subscribe to specific store
federation.subscribeStore('user', (userState) => {
console.log('User changed:', userState);
});
Persist state to localStorage with advanced options.
import { persist } from '@oxog/state';
const store = createStore({ count: 0, temp: '' })
.use(persist({
key: 'my-app',
storage: localStorage,
// v1.2.0 options
version: 1,
migrate: (state, version) => state,
partialize: (state) => ({ count: state.count }), // Only persist count
writeDebounce: 100,
onRehydrateStorage: (state) => console.log('Hydrated:', state),
}));
Connect to Redux DevTools Extension.
import { devtools } from '@oxog/state';
const store = createStore({ count: 0 })
.use(devtools({ name: 'My Store' }));
Add undo/redo functionality.
import { history } from '@oxog/state';
const store = createStore({ count: 0 })
.use(history({ limit: 50 }));
store.setState({ count: 1 });
store.setState({ count: 2 });
store.undo(); // { count: 1 }
store.redo(); // { count: 2 }
Synchronize state across browser tabs.
import { sync } from '@oxog/state';
const store = createStore({ count: 0 })
.use(sync({ channel: 'my-app' }));
Development logging with diff, timestamps, and filtering.
import { logger } from '@oxog/state';
const store = createStore({ count: 0 })
.use(logger({
level: 'debug',
collapsed: true,
diff: true,
timestamp: true,
filter: (action, state) => state.count > 0,
}));
Reactive side effects with debounce and cleanup.
import { effects, createEffect } from '@oxog/state';
const store = createStore({
searchTerm: '',
results: [],
})
.use(effects({
search: createEffect(
(state) => state.searchTerm,
async (term, { setState, signal }) => {
const res = await fetch(`/api/search?q=${term}`, { signal });
const results = await res.json();
setState({ results });
},
{ debounce: 300 }
),
}));
Schema-agnostic validation with Zod, Yup, or custom validators.
import { validate } from '@oxog/state';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().min(0).max(150),
});
const store = createStore({ email: '', age: 0 })
.use(validate({
schema,
timing: 'before',
rejectInvalid: true,
onError: (errors) => console.error(errors),
}));
Handle async operations with async actions.
const store = createStore({
data: null,
loading: false,
error: null,
$fetch: async (state, url: string) => {
store.setState({ loading: true, error: null });
try {
const res = await fetch(url);
const data = await res.json();
return { data, loading: false };
} catch (error) {
return { error, loading: false };
}
},
});
await store.getState().fetch('/api/users');
Full TypeScript support with type inference.
import { createStore, createSlice, InferSliceState } from '@oxog/state';
// Type inference from slices
const counterSlice = createSlice('counter', {
count: 0,
$increment: (state) => ({ count: state.count + 1 }),
});
type CounterState = InferSliceState<typeof counterSlice>;
// { counter: { count: number } }
// Explicit types
interface State {
count: number;
user: User | null;
}
const store = createStore<State>({
count: 0,
user: null,
$increment: (s) => ({ count: s.count + 1 }),
$setUser: (s, user: User) => ({ user }),
});
Testing utilities for stores.
import { createTestStore, mockStore, getStoreSnapshot } from '@oxog/state/testing';
// Isolated test store
const store = createTestStore({
count: 0,
$increment: (s) => ({ count: s.count + 1 }),
});
// Mock store with action tracking
const mock = mockStore({ count: 0, $increment: (s) => ({ count: s.count + 1 }) });
mock.getState().increment();
expect(mock.actionCalls.increment).toBe(1);
// Snapshot testing
const snapshot = getStoreSnapshot(store);
expect(snapshot).toMatchSnapshot();
Full documentation available at state.oxog.dev
MIT