@jucie.io/state
v1.0.24
Published
Modular state management system with path-based access, history, and reactive plugins
Maintainers
Readme
@jucie.io/state
A powerful state management system for JavaScript applications featuring path-based access, history management, and serialization capabilities.
Features
- 🎯 Path-Based Access: Intuitive nested object and array manipulation
- ⚡ High Performance: Optimized for frequent updates with minimal overhead
- 📝 History Management: Built-in undo/redo via HistoryManager plugin
- 💾 Serialization: Import/export state with CBOR encoding for persistence
- 🔍 Powerful Queries: Built-in querying with filters and transformations
- 🔌 Plugin Architecture: Extensible with HistoryManager, Matcher, OnChange, and custom plugins
- 🧪 Well Tested: Comprehensive test suite with performance benchmarks
- 🌊 Batch Operations: Efficient batch updates with change consolidation
License and Usage
This software is provided under the MIT License with Commons Clause.
✅ What You Can Do
- Use this library freely in personal or commercial projects
- Include it in your paid products and applications
- Modify and fork for your own use
- View and learn from the source code
❌ What You Cannot Do
- Sell this library as a standalone product or competing state management solution
- Offer it as a paid service (SaaS) where the primary value is this library
- Create a commercial fork that competes with this project
⚠️ No Warranty or Support
This software is provided "as-is" without any warranty, support, or guarantees:
- No obligation to provide support or answer questions
- No obligation to accept or implement feature requests
- No obligation to review or merge pull requests
- No obligation to fix bugs or security issues
- No obligation to maintain or update the software
You are welcome to submit issues and pull requests, but there is no expectation they will be addressed. Use this software at your own risk.
See the LICENSE file for complete terms.
Installation
npm install @jucio.io/stateQuick Start
import { createState } from '@jucio.io/state';
// Create a state instance
const state = createState();
// Set some initial data
state.set(['user'], { name: 'Alice', age: 30 });
state.set(['counter'], 0);
// Get values
console.log(state.get(['user'])); // { name: 'Alice', age: 30 }
console.log(state.get(['counter'])); // 0
// Update state
state.set(['user', 'age'], 31);
console.log(state.get(['user', 'age'])); // 31
// Update using a function
state.update(['counter'], count => count + 1);
console.log(state.get(['counter'])); // 1Core Concepts
State Management
The state system uses path-based access for nested data structures:
import { createState } from '@jucio.io/state';
const state = createState({
user: { name: 'Alice', profile: { age: 30 } },
items: ['apple', 'banana']
});
// Get values
const user = state.get(['user']); // { name: 'Alice', profile: { age: 30 } }
const name = state.get(['user', 'name']); // 'Alice'
const age = state.get(['user', 'profile', 'age']); // 30
// Set values
state.set(['user', 'name'], 'Bob');
state.set(['user', 'profile', 'age'], 25);
state.set(['items', 2], 'cherry'); // ['apple', 'banana', 'cherry']
// Multiple gets
const [userName, userAge] = state.get(['user', 'name'], ['user', 'profile', 'age']);Batch Operations
API Reference
State Creation
createState(initialState?)
Create a new state instance.
const state = createState({
user: { name: 'Alice' },
counter: 0
});State Operations
get(...paths)
Get values from state using path arrays.
// Single path
const user = state.get(['user']);
// Multiple paths
const [name, age] = state.get(['user', 'name'], ['user', 'age']);
// Works with arrays
const firstItem = state.get(['items', 0]);set(path, value)
Set a value at the specified path.
state.set(['user', 'name'], 'Bob');
state.set(['items', 0], 'apple');
state.set(['deeply', 'nested', 'value'], 42);update(path, updater)
Update a value using a function.
// Increment counter
state.update(['counter'], count => count + 1);
// Update object properties
state.update(['user'], user => ({ ...user, lastSeen: Date.now() }));
// Update array
state.update(['items'], items => [...items, 'new item']);remove(path)
Remove a value from state.
state.remove(['user', 'age']); // Remove specific property
state.remove(['items', 1]); // Remove array elementhas(...paths)
Check if paths exist in state.
const hasUser = state.has(['user']); // true/false
const [hasName, hasAge] = state.has(['user', 'name'], ['user', 'age']);keys(...paths)
Get object keys at specified paths.
const userKeys = state.keys(['user']); // ['name', 'profile']
const [userKeys, profileKeys] = state.keys(['user'], ['user', 'profile']);typeof(...paths)
Get the type of values at specified paths.
const userType = state.typeof(['user']); // 'object'
const nameType = state.typeof(['user', 'name']); // 'string'
const itemsType = state.typeof(['items']); // 'array'Batch Operations
batch(fn?)
Batch multiple state changes to minimize re-computations.
// Option 1: With callback (automatic)
state.batch(() => {
state.set(['user', 'name'], 'Charlie');
state.set(['user', 'age'], 35);
state.set(['counter'], 10);
// Batch automatically ends when callback completes
});
// Option 2: Manual control
const endBatch = state.batch();
state.set(['user', 'name'], 'Charlie');
state.set(['user', 'age'], 35);
state.set(['counter'], 10);
endBatch(); // Manually end the batchQueries
The state system provides tree-searching capabilities using findWhere and findAllWhere:
findWhere(key, matcher, value)
Find the first path where a key matches a condition.
// Find first user with role 'admin'
const adminPath = state.findWhere('role', 'is', 'admin');
// Returns: ['users', 0] (path to the matching node)
// Then get the value
if (adminPath) {
const admin = state.get(adminPath);
}
// Other matchers
state.findWhere('age', '>', 18); // Greater than
state.findWhere('age', '>=', 18); // Greater than or equal
state.findWhere('status', '!==', 'inactive'); // Not equalSupported matchers:
'is','===','=='- Equality'not','!==','!='- Inequality'>','gt'- Greater than'<','lt'- Less than'>=','gte'- Greater than or equal'<=','lte'- Less than or equal'includes'- Array includes value'has'- Object has value'in'- Key exists in object
findAllWhere(key, matcher, value)
Find all paths where a key matches a condition.
// Find all users with active status
const activePaths = state.findAllWhere('active', 'is', true);
// Returns: [['users', 0], ['users', 2], ['users', 5]]
// Get all matching values
const activeUsers = activePaths.map(path => state.get(path));Note: These methods search the entire state tree recursively and return paths, not values. Use state.get(path) to retrieve the actual data.
Plugins
The state system has a powerful plugin architecture that enables features like undo/redo and change tracking.
Installing Plugins
Plugins are installed using the install() method:
import { createState } from '@jucio.io/state';
import { HistoryManager } from '@jucio.io/state/history';
import { Matcher } from '@jucio.io/state/matcher';
const state = createState();
// Install a single plugin
state.install(HistoryManager);
// Install multiple plugins
state.install(HistoryManager, Matcher);HistoryManager Plugin
Provides undo/redo functionality with change tracking.
import { HistoryManager } from '@jucio.io/state/history';
const state = createState();
state.install(HistoryManager);
state.set(['counter'], 1);
state.set(['counter'], 2);
state.set(['counter'], 3);
// Undo operations
state.history.undo(); // counter back to 2
state.history.undo(); // counter back to 1
// Redo operations
state.history.redo(); // counter back to 2
// Check history status
console.log(state.history.canUndo()); // true/false
console.log(state.history.canRedo()); // true/false
console.log(state.history.size()); // number of history entries
// Batch history changes
const unbatch = state.history.batch();
state.set(['user', 'name'], 'Alice');
state.set(['user', 'age'], 30);
unbatch(); // Commits all changes as single history entry
// Add custom markers for better history navigation
state.set(['step'], 1);
state.history.addMarker('Step 1 completed');
state.set(['step'], 2);
state.history.addMarker('Step 2 completed');
// Listen to history commits
const unsubscribe = state.history.onCommit((changes) => {
console.log('History committed:', changes);
});Configuration Options:
import { HistoryManager } from '@jucio.io/state/history';
// Configure with custom options
state.install(HistoryManager.configure({
maxSize: 200 // Maximum history entries (default: 100)
}));Creating Custom Plugins
You can create custom plugins by extending the Plugin base class:
import { Plugin } from '@jucio.io/state/Plugin';
class CustomPlugin extends Plugin {
static name = 'custom';
static options = {
customOption: 'default'
};
initialize(state, options) {
// Called once when plugin is installed
state.addChangeListener((marker, change) => {
// React to changes
});
}
actions(state) {
// Return methods available on state.custom.*
return {
myAction: () => {
// Custom functionality
}
};
}
reset() {
// Called when state.reset() is invoked
}
}
// Use the plugin
state.install(CustomPlugin);
state.custom.myAction();Serialization
Export and Import
Both export() and import() are async methods that use CBOR encoding.
// Export state to CBOR format (async)
const exported = await state.export();
// Import into new state
const newState = createState();
await newState.import(exported);
// Export specific path
const userExport = await state.export(['user']);Change Tracking
Change tracking is available through the @jucio.io/state/on-change plugin:
import { createState } from '@jucio.io/state';
import { OnChange } from '@jucio.io/state/on-change';
const state = createState();
state.install(OnChange);
const unsubscribe = state.onChange.addListener((changes) => {
changes.forEach(change => {
console.log(`${change.method} at ${change.path.join('.')}`);
});
});
// Later, unsubscribe
unsubscribe();Advanced Usage
Change Tracking
Listen to all state changes using the OnChange plugin:
import { OnChange } from '@jucio.io/state/on-change';
state.install(OnChange);
const unsubscribe = state.onChange.addListener((changes) => {
changes.forEach(change => {
console.log(`${change.method} at ${change.path.join('.')}`);
});
});
// Later, unsubscribe
unsubscribe();Custom Effects
Track specific state changes using the Matcher plugin:
import { Matcher, createMatcher } from '@jucio.io/state/matcher';
state.install(Matcher);
const unsubscribe = state.matcher.createMatcher(['user'], (user) => {
console.log('User data changed:', user);
});
// Clean up when done
unsubscribe();Complex State Structures
const state = createState({
app: {
theme: 'dark',
language: 'en'
},
users: [
{ id: 1, name: 'Alice', role: 'admin', active: true },
{ id: 2, name: 'Bob', role: 'user', active: false },
{ id: 3, name: 'Charlie', role: 'user', active: true }
],
posts: [
{ id: 1, authorId: 1, title: 'Hello World', likes: 5 },
{ id: 2, authorId: 2, title: 'JavaScript Tips', likes: 12 }
]
});
// Find active users
const activePaths = state.findAllWhere('active', 'is', true);
const activeUsers = activePaths.map(path => state.get(path));
// Calculate total likes manually
const posts = state.get(['posts']);
const totalLikes = posts.reduce((sum, p) => sum + p.likes, 0);
// Get dashboard stats
const users = state.get(['users']);
const dashboardStats = {
activeUserCount: users.filter(u => u.active).length,
totalPosts: posts.length,
totalLikes: totalLikes
};Performance Optimization
Batch Operations for Performance
// Inefficient - triggers multiple change events
state.set(['users', 0, 'name'], 'Alice Updated');
state.set(['users', 0, 'email'], '[email protected]');
state.set(['users', 0, 'lastLogin'], Date.now());
// Efficient - single batched operation
state.batch(() => {
state.set(['users', 0, 'name'], 'Alice Updated');
state.set(['users', 0, 'email'], '[email protected]');
state.set(['users', 0, 'lastLogin'], Date.now());
});
// Or use update for object modifications
state.update(['users', 0], user => ({
...user,
name: 'Alice Updated',
email: '[email protected]',
lastLogin: Date.now()
}));Best Practices
1. Use Path Arrays Consistently
// ✅ Good - consistent path format
state.get(['user', 'profile', 'name']);
state.set(['user', 'profile', 'name'], 'Alice');
// ❌ Avoid - mixing path formats
state.get('user.profile.name'); // This won't work2. Batch Related Updates
// ✅ Good - batched updates
state.batch(() => {
state.set(['user', 'name'], 'Alice');
state.set(['user', 'email'], '[email protected]');
state.set(['user', 'updatedAt'], Date.now());
});
// ❌ Avoid - separate updates
state.set(['user', 'name'], 'Alice');
state.set(['user', 'email'], '[email protected]');
state.set(['user', 'updatedAt'], Date.now());3. Use Standard JavaScript for Derived Data
// ✅ Good - use get() and standard JS
const users = state.get(['users']);
const fullNames = users.map(u => `${u.firstName} ${u.lastName}`);
// ✅ Also good - compute on demand
function getFullName(userId) {
const users = state.get(['users']);
const user = users.find(u => u.id === userId);
return user ? `${user.firstName} ${user.lastName}` : null;
}4. Clean Up Effects and Listeners
// ✅ Good - cleanup
class Component {
constructor() {
this.unsubscribe = state.onChange.addListener(this.handleChange.bind(this));
}
destroy() {
this.unsubscribe();
}
}Performance
The library is highly optimized for real-world performance:
Benchmarks
State Operations:
get (simple): 7.6M ops/sec
set (simple): 4.8M ops/sec
get (nested): 5.0M ops/sec
set (nested): 3.6M ops/sec
Plugins:
HistoryManager: ~12% overhead on writes, 0% on readsPerformance Considerations
- Path-based access: More efficient than deep object watches
- Fine-grained tracking: Only track changes where needed
- Batching: Use batch operations to consolidate multiple changes
- Memory: Clean up listeners when components are destroyed
- Plugins: Minimal overhead - gets remain fast, writes have reasonable tracking cost
Testing
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run benchmarks
npm run benchLicense
This project is licensed under the MIT License with Commons Clause.
This means you can freely use this library in your projects (including commercial ones), but you cannot sell the library itself as a standalone product or competing service.
See the LICENSE file for complete details.
Contributing
You are welcome to submit issues and pull requests, however:
- There is no guarantee that issues will be addressed
- There is no guarantee that pull requests will be reviewed or merged
- This project is maintained on an as-available basis with no commitments
By contributing, you agree that your contributions will be licensed under the same MIT + Commons Clause license.
