zust
v1.0.6
Published
A powerful, lightweight, and fully standalone state management library for React with time-travel debugging, computed values, and zero dependencies
Maintainers
Readme
Zust State Management
A powerful, lightweight, and fully standalone state management library for React applications.
Zust provides an intuitive API for managing complex application state with advanced features like time-travel debugging, computed values, async operations, and more - all with zero dependencies (except React).
✨ Features
- 🎯 Zero Dependencies - Fully standalone, no external dependencies
- 🚀 Lightweight - Minimal bundle size with maximum performance
- 🔥 Type-Safe - Full TypeScript support with excellent type inference
- 🎨 Intuitive API - Simple dot-notation paths for nested state updates
- 📦 Array Support - Native support for array indices in paths (
"todos.0.done") - ⏱️ Time-Travel - Built-in undo/redo with history management
- 🧮 Computed Values - MobX-style cached computed properties
- ⚡ Async Actions - First-class async/await support with dispatch
- 🔔 Granular Subscriptions - Subscribe to specific paths for optimal performance
- 🎛️ Batched Updates - Automatic batching to minimize re-renders
- 💾 Persistence - Built-in localStorage/sessionStorage support
- 🔒 Secure - Protection against prototype pollution attacks
- 🧩 Extensible - Middleware and plugin system for customization
Live Example
Check out the interactive example app in the example/ folder to see all features in action. To run it:
cd example
npm install # or bun install
npm run dev # or bun devThen open http://localhost:3000 to see the interactive demo.
Table of Contents
- Installation
- Quick Start
- Core Concepts
- Advanced Features
- Persistence
- API Reference
- Migration Guide
- License
- Contributors
Installation
npm install zustor
bun install zustQuick Start
import { createStore } from 'zust';
// Define your initial state
const initialState = {
user: { name: 'John', age: 30 },
todos: [
{ id: 1, text: 'Learn Zust', done: false },
{ id: 2, text: 'Build app', done: false }
],
settings: { theme: 'light' as 'light' | 'dark' },
};
// Create the store
const { useSelectors, setDeep, getState } = createStore(initialState);
function App() {
// Select multiple values efficiently
const { name, theme } = useSelectors('user.name', 'settings.theme');
// Update nested state with ease
const updateName = () => setDeep('user.name', 'Jane');
const toggleTheme = () => setDeep('settings.theme',
prev => prev === 'light' ? 'dark' : 'light'
);
return (
<div>
<p>User: {name}</p>
<p>Theme: {theme}</p>
<button onClick={updateName}>Update Name</button>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}Core Concepts
Basic Usage
Zust uses dot-notation paths to access and update deeply nested state:
const { setDeep, getState } = createStore({
user: {
profile: {
name: 'John',
email: '[email protected]'
},
preferences: {
notifications: true
}
}
});
// Update nested values
setDeep('user.profile.name', 'Jane');
setDeep('user.preferences.notifications', false);
// Use functional updates
setDeep('user.profile.name', prevName => prevName.toUpperCase());
// Access current state
const currentState = getState();
console.log(currentState.user.profile.name); // 'JANE'Array Paths
Zust has native support for array indices in paths:
const { setDeep } = createStore({
todos: [
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: false }
]
});
// Update array items using index notation
setDeep('todos.0.done', true);
setDeep('todos.1.text', 'Updated Task 2');
// Works with nested arrays
setDeep('matrix.0.1.value', 42);Async Operations
Dispatch async actions with first-class async/await support:
const store = createStore({
data: null,
loading: false,
error: null
});
const state = store.getState();
// Dispatch async actions
await state.dispatch(async (state, setDeep) => {
setDeep('loading', true);
try {
const response = await fetch('/api/data');
const data = await response.json();
setDeep('data', data);
} catch (error) {
setDeep('error', error.message);
} finally {
setDeep('loading', false);
}
});Advanced Features
Time-Travel Debugging
Enable undo/redo functionality with built-in history management:
const { getState, history } = createStore(
{ counter: 0 },
{
history: {
enabled: true,
maxSize: 50, // Maximum history entries (default: 50)
debounceMs: 100 // Debounce captures (default: 100ms)
}
}
);
const state = getState();
// Make some changes
setDeep('counter', 1);
setDeep('counter', 2);
setDeep('counter', 3);
// Undo/redo
if (history?.canUndo()) {
history.undo(); // counter is now 2
}
if (history?.canRedo()) {
history.redo(); // counter is back to 3
}
// Jump to specific state
history?.jump(-2); // Go back 2 states
// Clear history
history?.clear();Computed Values
Define cached computed properties that automatically recompute when dependencies change:
const { getState } = createStore(
{
firstName: 'John',
lastName: 'Doe',
items: [{ price: 10 }, { price: 20 }]
},
{
computedValues: {
// Simple computed value
fullName: (state) => `${state.firstName} ${state.lastName}`,
// Computed value with explicit dependencies
total: {
compute: (state) => state.items.reduce((sum, item) => sum + item.price, 0),
deps: ['items'], // Only recompute when items change
cache: true // Cache the result (default: true)
}
}
}
);
const state = getState();
console.log(state.fullName); // 'John Doe'
console.log(state.total); // 30
// Computed values update automatically
setDeep('firstName', 'Jane');
console.log(getState().fullName); // 'Jane Doe'Path-Based Subscriptions
Subscribe to changes on specific paths for optimal performance:
const { subscribePath } = createStore({
user: { name: 'John', age: 30 },
settings: { theme: 'light' }
});
// Subscribe to specific path
const unsubscribe = subscribePath('user.name', (newValue, oldValue, fullState) => {
console.log(`Name changed from ${oldValue} to ${newValue}`);
});
setDeep('user.name', 'Jane'); // Triggers callback
setDeep('user.age', 31); // Does NOT trigger callback
// Unsubscribe when done
unsubscribe();Batched Updates
Batch multiple updates to minimize re-renders:
import { batch } from 'zust';
const { setDeep, subscribe } = createStore({ a: 0, b: 0, c: 0 });
let renderCount = 0;
subscribe(() => renderCount++);
// Without batching: 3 renders
setDeep('a', 1);
setDeep('b', 2);
setDeep('c', 3);
// With batching: 1 render
batch(() => {
setDeep('a', 1);
setDeep('b', 2);
setDeep('c', 3);
});Persistence
Persist state to localStorage or sessionStorage:
import { createStore, createPersistConfig } from 'zust';
const { useSelectors, setDeep } = createStore(
{
user: { name: 'John', age: 30 },
settings: { theme: 'light', language: 'en' }
},
{
persist: createPersistConfig('user', 'settings.theme'),
prefix: 'myapp' // localStorage key prefix
}
);
// Persist entire store
const store2 = createStore(initialState, { persist: true });API Reference
createStore<T>(initialState, options?)
Creates a Zust store with the provided initial state and options.
Parameters:
initialState: T- The initial state object (must be non-null object)options?: StoreOptions<T>- Configuration options
Returns: StoreCreationResult<T> containing:
useStore()- React hook that returns the enhanced storeuseSelectors(...paths)- Hook to select multiple state valuesgetState()- Get current state with methods (dispatch, setDeep, etc.)setState(partial, replace?)- Set state (shallow or deep merge)setDeep(path, value)- Update nested state by pathsubscribe(listener)- Subscribe to all state changessubscribePath(path, callback)- Subscribe to specific path changesdestroy()- Cleanup and destroy the storehistory?- History API (if history is enabled)
StoreOptions<T>
persist?: boolean | PersistConfig<T>- Enable state persistenceprefix?: string- Prefix for localStorage keyslogging?: boolean- Enable console loggingmiddleware?: Middleware<T>[]- Array of middleware functionscomputedValues?: ComputedValues<T>- Computed properties definitionplugins?: Plugin<T>[]- Store pluginshistory?: HistoryConfig- Time-travel debugging configuration
HistoryConfig
enabled: boolean- Enable history trackingmaxSize?: number- Maximum history entries (default: 50)debounceMs?: number- Debounce delay for captures (default: 100ms)
batch(fn: () => void)
Batch multiple state updates into a single notification.
Enhanced Store Methods
The store returned by getState() or useStore() includes:
setDeep(path, action)- Update state by pathdispatch(asyncAction)- Execute async actionssubscribe(listener)- Subscribe to state changessubscribePath(path, callback)- Subscribe to path changesdeleteDeep(path)- Delete property by pathhasPath(path)- Check if path existshistory?- Undo/redo API (if enabled)
Migration Guide
From Version 0.x
Version 1.0 is a complete rewrite with breaking changes:
What's New:
- ✅ Fully standalone (no Zustand dependency)
- ✅ Array path support
- ✅ Time-travel debugging
- ✅ Async dispatch
- ✅ Computed values with caching
- ✅ Path-based subscriptions
- ✅ Better TypeScript types
Breaking Changes:
- Removed Zustand middleware compatibility
getState()now returns enhanced store (not raw state)- Internal engine completely rewritten
Migration Steps:
- Update imports (API is mostly backward compatible)
- Remove any Zustand-specific middleware
- Update TypeScript types if using advanced features
- Test your application thoroughly
Tests
Zust includes comprehensive tests:
- All 69 tests passing ✅
- Integration tests with React
- Unit tests for all features
- Security tests (prototype pollution protection)
- Edge case handling
Run tests:
npm testLicense
MIT License. See the LICENSE file for details.
Contributors ✨
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
