- Introduction & Philosophy
- Getting Started
- React Integration
- Transactional State
- Middleware & Observability
- Store Composition
- Validation & Error Handling
- Server-Side Rendering (SSR)
- Comparison with Other Libraries
- API Reference
Exostate is a next-generation state management library designed for backend-grade reliability in frontend applications. Our philosophy centers around three core principles:
- Zero tolerance for
anytypes - Full TypeScript generics throughout
- Compile-time guarantees for runtime safety
- Inspired by database transaction models
- Atomic commit/rollback semantics
- Consistent state guarantees during complex operations
- Framework-agnostic core (works with React, Vue, Svelte, Vanilla JS)
- Server-side rendering ready
- Node.js and browser compatible
Traditional state management libraries often prioritize simplicity over reliability. Exostate brings the rigor of backend systems to frontend state management:
- For complex applications: When your state logic involves multi-step operations, conditional updates, or error recovery
- For mission-critical systems: When data consistency and reliability are non-negotiable
- For teams that value types: When you want compile-time guarantees about your state transitions
npm install exostateFor React applications:
npm install exostate react react-domimport { createStore } from 'exostate';
// Define your state interface
interface CounterState {
count: number;
loading: boolean;
}
// Create a store with initial state
const counterStore = createStore<CounterState>({
count: 0,
loading: false
});
// Read current state
console.log(counterStore.read()); // { count: 0, loading: false }
// Update state using a reducer
counterStore.update((state, increment: number) => ({
...state,
count: state.count + increment
}), 5);
console.log(counterStore.read().count); // 5// Subscribe to count changes
const unsubscribe = counterStore.subscribe(
(state) => state.count,
(count) => console.log('Count changed:', count)
);
// Later, unsubscribe
unsubscribe();Exostate provides first-class React hooks through the exostate/react subpath.
import { createStore } from 'exostate';
import { useStore, useSelector } from 'exostate/react';
const userStore = createStore({
user: null,
preferences: { theme: 'dark' }
});
function UserProfile() {
// Access entire store state
const state = useStore(userStore);
// Or use selector for granular updates
const theme = useSelector(userStore, state => state.preferences.theme);
return (
<div className={theme}>
<h1>{state.user?.name}</h1>
</div>
);
}import { useStores } from 'exostate/react';
function App() {
const { auth, settings, notifications } = useStores({
auth: authStore,
settings: settingsStore,
notifications: notificationsStore
});
return (
<div className={settings.theme}>
{auth.user ? <Dashboard /> : <Login />}
</div>
);
}function ExpensiveComponent() {
// Only re-renders when specific user properties change
const userName = useSelector(
userStore,
state => state.user?.name,
(prev, next) => prev === next // Custom equality check
);
return <div>{userName}</div>;
}Exostate's transaction system provides atomic operations with rollback capabilities.
import { beginTransaction } from 'exostate';
const store = createStore({ balance: 100, transactions: [] });
function transferFunds(amount: number) {
const tx = beginTransaction(store);
try {
// Multiple operations in transaction
tx.update(state => ({
balance: state.balance - amount,
transactions: [...state.transactions, { amount, type: 'debit' }]
}), undefined);
// Additional business logic...
// Commit if everything succeeds
tx.commit();
console.log('Transfer successful');
} catch (error) {
// Automatic rollback on error
tx.rollback();
console.log('Transfer failed, state restored');
}
}function complexOperation() {
const outerTx = beginTransaction(store);
try {
// First operation
outerTx.update(state => ({ ...state, step: 1 }), undefined);
// Nested transaction for specific part
const innerTx = beginTransaction(store);
try {
innerTx.update(state => ({ ...state, detail: 'nested' }), undefined);
innerTx.commit();
} catch {
innerTx.rollback();
throw new Error('Nested operation failed');
}
outerTx.commit();
} catch {
outerTx.rollback();
}
}Exostate's middleware system allows intercepting operations for logging, metrics, and side effects.
import { withMiddleware } from 'exostate';
const loggingMiddleware = {
before: (operation, context) => {
console.log(`Starting ${operation}`, {
version: context.version,
snapshot: context.snapshot,
payload: context.payload
});
},
after: (operation, context) => {
console.log(`Completed ${operation} in ${context.durationMs}ms`, {
version: context.version,
snapshot: context.snapshot
});
}
};
const monitoredStore = withMiddleware(store, [loggingMiddleware]);const metricsMiddleware = {
after: (operation, context) => {
// Send metrics to monitoring service
trackMetric({
operation,
duration: context.durationMs,
stateSize: JSON.stringify(context.snapshot).length
});
}
};import { devtoolsMiddleware, createMemoryConnection } from 'exostate';
const connection = createMemoryConnection();
const devToolsStore = withMiddleware(store, [
devtoolsMiddleware(connection, 'my-app')
]);
// Connect to devtools UI
connection.connect(devToolsUI);Combine multiple stores into a unified state tree.
import { combineStores } from 'exostate';
const authStore = createStore({ user: null, token: null });
const uiStore = createStore({ theme: 'dark', sidebarOpen: false });
const dataStore = createStore({ items: [], loading: false });
const rootStore = combineStores({
auth: authStore,
ui: uiStore,
data: dataStore
});
// Access combined state
const state = rootStore.read();
console.log(state.auth.user, state.ui.theme, state.data.items);import { useStores } from 'exostate/react';
function AppLayout() {
const { auth, ui, data } = useStores({
auth: authStore,
ui: uiStore,
data: dataStore
});
return (
<div className={ui.theme}>
<Sidebar open={ui.sidebarOpen} />
<MainContent user={auth.user} items={data.items} />
</div>
);
}// Only subscribe to auth changes
rootStore.subscribe(
state => state.auth,
authState => console.log('Auth changed:', authState)
);import { z } from 'zod';
import { fromZod } from 'exostate';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest'])
});
const userValidator = fromZod(UserSchema);
// Validate state updates
function updateUser(newUser: unknown) {
if (userValidator.validate(newUser)) {
userStore.set(newUser);
} else {
throw new Error('Invalid user data');
}
}import { createError, isSafeError, toSafeError } from 'exostate';
// Create typed errors
const AppError = createError('app_error', 'Application error');
const ValidationError = createError('validation_error', 'Validation failed');
// Error handling in transactions
try {
const tx = beginTransaction(store);
// ... operations
tx.commit();
} catch (error) {
if (isSafeError(error)) {
console.log(`Error ${error.code}: ${error.message}`);
} else {
// Convert to safe error
const safeError = toSafeError(error, 'unknown_error');
console.log('Unknown error:', safeError.message);
}
}const errorPolicy = {
map: (code: string, error: SafeError) => {
switch (code) {
case 'network_error':
return createError('retry_later', 'Please try again later');
case 'validation_error':
return createError('invalid_input', 'Please check your input');
default:
return error;
}
}
};
// Apply policy to errors
const userFriendlyError = applyPolicy(rawError, errorPolicy);// Server-side
import { dehydrate } from 'exostate';
function renderApp() {
const store = createStore(initialState);
// ... populate store
const html = renderToString(<App store={store} />);
const serializedState = dehydrate(store);
return {
html,
state: serializedState
};
}// Client-side
import { rehydrate } from 'exostate';
function hydrateApp() {
const store = createStore(initialState);
// Rehydrate from server state
if (window.__INITIAL_STATE__) {
rehydrate(store, window.__INITIAL_STATE__);
}
ReactDOM.hydrate(<App store={store} />, container);
}import { createSerializer } from 'exostate';
const customSerializer = createSerializer(2, {
validate: (x): x is AppState =>
typeof x?.version === 'number' && Array.isArray(x?.items),
migrations: {
1: (v1Data: any) => ({
version: 2,
items: v1Data.products || []
})
}
});
const serialized = dehydrate(store, customSerializer);| Feature | Exostate | Zustand |
|---|---|---|
| Transactions | ✅ Full commit/rollback | ❌ No built-in support |
| Type Safety | ✅ Strict generics, no any |
any |
| Middleware | ✅ Full lifecycle hooks | ✅ Basic middleware |
| SSR | ✅ First-class support | ✅ Good support |
| Complexity | 🟡 Medium (more features) | 🟢 Simple |
| Use Case | Complex apps, mission-critical | Simple to medium apps |
| Feature | Exostate | Redux |
|---|---|---|
| Boilerplate | 🟢 Minimal | 🔴 High |
| Type Safety | ✅ Excellent | 🟡 Good (with RTK) |
| Transactions | ✅ Native support | ❌ Requires middleware |
| Performance | ✅ Optimized updates | 🟡 Good (with selectors) |
| Learning Curve | 🟡 Moderate | 🔴 Steep |
| Bundle Size | 🟢 Small (~3kB) | 🔴 Large (~10kB+) |
Benchmarks run on Node.js (higher is better). Exostate leads in raw throughput and with subscribers when reducers use Object.assign for updates.
| Library | Scenario | Ops/sec |
|---|---|---|
| Exostate | update (Object.assign) |
~19,800,000 |
| Zustand | setState | ~14,800,000 |
| Exostate | update ({...spread}) |
~8,000,000 |
| Redux | dispatch | ~5,500,000 |
| Exostate | sub (Object.assign) |
~17,400,000 |
| Zustand | sub | ~15,000,000 |
| Exostate | sub ({...spread}) |
~7,300,000 |
| Redux | sub | ~5,000,000 |
Quickly reproduce locally:
npm installnpm run buildnode benchmarks/comparison.mjs
Notes:
- Using
Object.assignin reducers minimizes allocation and consistently delivers the highest throughput. - Exostate’s subscriber path remains predictable and fast due to a Set-based listener model with copy-on-write safety.
- ✅ Complex state logic with multiple dependent updates
- ✅ Mission-critical applications where data consistency is vital
- ✅ TypeScript-heavy projects wanting maximum type safety
- ✅ Applications needing audit logging or operation tracking
- ✅ Teams familiar with backend development patterns
Creates a new store with initial state.
interface Store<T> {
read(): T;
snapshot(): DeepReadonly<T>;
update<P>(reducer: Reducer<T, P>, payload: P): T;
set(next: T): T;
compute(fn: Compute<T>): T;
batch(apply: (apply: Function) => void): T;
subscribe<R>(selector: Selector<T, R>, subscriber: Subscriber<R>, options?: SubscribeOptions<R>): Unsubscribe;
}Starts a new transaction.
interface Transaction<T> {
read(): T;
apply<P>(reducer: Reducer<T, P>, payload: P): T;
compute(fn: Compute<T>): T;
set(next: T): T;
commit(): T;
rollback(): T;
}Hook to access entire store state.
Hook to access derived state with optional equality check.
useStores<TShape>(stores: { [K in keyof TShape]: Store<TShape[K]> }): { [K in keyof TShape]: DeepReadonly<TShape[K]> }
Hook to combine multiple stores.
Combines multiple stores into one.
Wraps store with middleware.
Serializes store state for SSR.
Restores store state from serialized data.
Creates a typed error instance.
Type guard for safe errors.
Converts unknown errors to safe errors.
const cartStore = createStore({
items: [],
discount: 0,
tax: 0,
total: 0
});
function addToCart(product: Product, quantity: number) {
const tx = beginTransaction(cartStore);
try {
// Add item
tx.update(state => ({
...state,
items: [...state.items, { product, quantity }]
}), undefined);
// Recalculate totals
tx.update(calculateTotals, undefined);
// Validate business rules
if (tx.read().items.length > 10) {
throw new Error('Cart limit exceeded');
}
tx.commit();
} catch (error) {
tx.rollback();
throw error;
}
}const authStore = createStore({
user: null,
token: null,
loading: false,
error: null
});
async function login(credentials: LoginData) {
const tx = beginTransaction(authStore);
try {
tx.set({ ...tx.read(), loading: true, error: null });
const response = await api.login(credentials);
tx.set({
user: response.user,
token: response.token,
loading: false,
error: null
});
tx.commit();
return response;
} catch (error) {
tx.set({
...tx.read(),
loading: false,
error: toSafeError(error, 'login_failed')
});
tx.commit();
throw error;
}
}