@jucie.io/state-matcher
v1.0.13
Published
Matcher plugin for @jucie.io/state with pattern matching and subscriptions
Maintainers
Readme
@jucio.io/state/matcher
Path-based state watching plugin for @jucio.io/state that allows you to watch specific paths in your state tree and react to changes. Your handlers receive the current data at the watched path, not change objects.
Features
- 🎯 Path Matching: Watch specific paths in your state tree
- 📊 Data Values: Handlers receive actual data at the path, not change objects
- 🌳 Hierarchical Watching: Match exact paths, parent paths, or child paths
- 📦 Automatic Batching: Changes are automatically batched and debounced
- 🔄 Smart Consolidation: Multiple changes to the same path are consolidated
- 🎬 Declarative Setup: Define matchers during state initialization
- 🔌 Plugin Architecture: Seamlessly integrates with @jucie.io/state
Installation
npm install @jucio.io/stateNote: Matcher plugin is included in the main package.
Quick Start
import { createState } from '@jucio.io/state';
import { Matcher, createMatcher } from '@jucio.io/state/matcher';
// Create a matcher
const userMatcher = createMatcher(['user'], (userData) => {
console.log('User data:', userData);
});
// Create state and install matcher plugin
const state = createState({
user: { name: 'Alice', age: 30 },
settings: { theme: 'dark' }
});
// Install with initial matchers
state.install(Matcher.configure({
matchers: [userMatcher]
}));
// Change user data - matcher receives the NEW data
state.set(['user', 'name'], 'Bob');
// Console: "User data: { name: 'Bob', age: 30 }"
// Change settings - matcher doesn't fire (different path)
state.set(['settings', 'theme'], 'light');API Reference
Creating Matchers
createMatcher(path, handler)
Create a matcher that watches a specific path in the state tree.
The handler receives the current data at the watched path, not change objects.
import { createMatcher } from '@jucio.io/state/matcher';
const matcher = createMatcher(['users', 'profile'], (profileData) => {
console.log('Profile is now:', profileData);
});Parameters:
path(Array): Path to watch (e.g.,['user'],['users', 'profile'])handler(Function): Callback function that receives the current data at the path
Returns: Matcher function that can be added to the plugin
Plugin Actions
When using the Matcher plugin with a state instance, you get access to these actions:
state.matcher.createMatcher(path, handler)
Create and automatically register a matcher.
import { createState } from '@jucio.io/state';
import { Matcher } from '@jucio.io/state/matcher';
const state = createState({ user: { name: 'Alice' } });
state.install(Matcher);
const unsubscribe = state.matcher.createMatcher(['user'], (userData) => {
console.log('User is now:', userData);
});
// Later: remove the matcher
unsubscribe();Returns: Unsubscribe function
state.matcher.addMatcher(matcher)
Add an existing matcher to the plugin.
const matcher = createMatcher(['user'], (userData) => {
console.log('User:', userData);
});
state.matcher.addMatcher(matcher);state.matcher.removeMatcher(matcher)
Remove a matcher from the plugin.
state.matcher.removeMatcher(matcher);Match Types
Matchers use hierarchical matching with three types. The handler always receives the current data at the matched path:
Exact Match
Watches the exact path specified:
const matcher = createMatcher(['user', 'profile'], (profileData) => {
console.log('Profile data:', profileData);
});
state.set(['user', 'profile'], { bio: 'Hello' }); // ✅ Fires with { bio: 'Hello' }
state.set(['user', 'profile', 'bio'], 'Hi'); // ✅ Fires with { bio: 'Hi' }
state.set(['user'], { profile: { bio: 'Hi' } }); // ✅ Fires with { bio: 'Hi' }
state.set(['user', 'settings'], {}); // ❌ Doesn't fireParent Match
Fires when a parent path or any descendant changes:
const matcher = createMatcher(['user'], (userData) => {
console.log('User data:', userData);
});
state.set(['user', 'name'], 'Alice'); // ✅ Fires with entire user object
state.set(['user', 'profile', 'bio'], 'Hello'); // ✅ Fires with entire user object
state.set(['user'], { name: 'Bob' }); // ✅ Fires with { name: 'Bob' }Child Match
When watching a parent and children change, child changes are consolidated:
const matcher = createMatcher(['users'], (usersData) => {
console.log('Users data:', usersData);
});
state.set(['users', 'alice'], { name: 'Alice' });
state.set(['users', 'bob'], { name: 'Bob' });
// Both changes are batched. Handler receives the full current state:
// { alice: { name: 'Alice' }, bob: { name: 'Bob' } }Configuration
Initialize with Matchers
import { createState } from '@jucio.io/state';
import { Matcher, createMatcher } from '@jucio.io/state/matcher';
const userMatcher = createMatcher(['user'], (user) => {
console.log('User:', user);
});
const settingsMatcher = createMatcher(['settings'], (settings) => {
console.log('Settings:', settings);
});
const state = createState({ user: {}, settings: {} });
state.install(Matcher.configure({
matchers: [userMatcher, settingsMatcher]
}));Define Matchers as Objects
import { createState } from '@jucio.io/state';
import { Matcher } from '@jucio.io/state/matcher';
const state = createState({ user: {}, settings: {} });
state.install(Matcher.configure({
matchers: [
{
path: ['user'],
handler: (user) => console.log('User:', user)
},
{
path: ['settings'],
handler: (settings) => console.log('Settings:', settings)
}
]
}));Advanced Usage
Multiple Matchers on Same Path
const logger = createMatcher(['user'], (userData) => {
console.log('User changed:', userData);
});
const validator = createMatcher(['user'], (userData) => {
if (!userData.email) {
console.warn('User has no email!');
}
});
state.matcher.addMatcher(logger);
state.matcher.addMatcher(validator);
state.set(['user'], { name: 'Alice' });
// Both matchers fire with { name: 'Alice' }Dynamic Matcher Management
// Add matcher conditionally
if (process.env.NODE_ENV === 'development') {
const debugMatcher = state.matcher.createMatcher(['*'], (data) => {
console.log('DEBUG: State changed:', data);
});
}
// Add/remove based on user settings
function toggleAuditLog(enabled) {
if (enabled) {
const auditMatcher = createMatcher(['data'], (data) => {
logToServer('data-change', data);
});
state.matcher.addMatcher(auditMatcher);
return () => state.matcher.removeMatcher(auditMatcher);
}
}Nested Path Watching
// Watch different levels of nesting
const userMatcher = createMatcher(['user'], (userData) => {
console.log('Entire user object:', userData);
});
const profileMatcher = createMatcher(['user', 'profile'], (profileData) => {
console.log('Just profile:', profileData);
});
const nameMatcher = createMatcher(['user', 'profile', 'name'], (name) => {
console.log('Just name:', name);
});
state.set(['user', 'profile', 'name'], 'Alice');
// All three matchers fire:
// - userMatcher gets the entire user object
// - profileMatcher gets just the profile object
// - nameMatcher gets just 'Alice'Batching and Consolidation
const matcher = createMatcher(['items'], (itemsData) => {
console.log('Items:', itemsData);
});
// Multiple rapid changes are batched
state.set(['items', 'item1'], { value: 1 });
state.set(['items', 'item2'], { value: 2 });
state.set(['items', 'item3'], { value: 3 });
// Single callback with current state:
// { item1: { value: 1 }, item2: { value: 2 }, item3: { value: 3 } }Common Patterns
Form Field Validation
const emailMatcher = createMatcher(['form', 'email'], (email) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
state.set(['form', 'errors', 'email'], isValid ? null : 'Invalid email');
});
state.matcher.addMatcher(emailMatcher);Persistence
const persistMatcher = createMatcher(['user', 'preferences'], (preferences) => {
localStorage.setItem('preferences', JSON.stringify(preferences));
});
state.matcher.addMatcher(persistMatcher);Analytics Tracking
const analyticsMatcher = createMatcher(['analytics', 'events'], (events) => {
Object.entries(events).forEach(([key, event]) => {
trackEvent(event.name, event.properties);
});
});
state.matcher.addMatcher(analyticsMatcher);Derived State Updates
// Update derived state when source changes
const cartMatcher = createMatcher(['cart', 'items'], (items) => {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
state.set(['cart', 'total'], total);
});
state.matcher.addMatcher(cartMatcher);API Synchronization
const syncMatcher = createMatcher(['user'], async (userData) => {
try {
await fetch('/api/user', {
method: 'PUT',
body: JSON.stringify(userData)
});
console.log('User synced to server');
} catch (error) {
console.error('Failed to sync user:', error);
}
});
state.matcher.addMatcher(syncMatcher);Performance Considerations
Automatic Batching: Matchers automatically batch changes using
setTimeout(fn, 0), so multiple synchronous changes trigger the handler only onceSmart Consolidation: Multiple changes to the same path are consolidated into a single update
Efficient Matching: Uses marker comparison for fast path matching
Cleanup: Always unsubscribe matchers when they're no longer needed to prevent memory leaks
// Good: Clean up when done
const unsubscribe = state.matcher.createMatcher(['temp'], handler);
// ... later
unsubscribe();Comparison with OnChange Plugin
| Feature | Matcher | OnChange | |---------|---------|----------| | What handler receives | Current data at path | Change objects with metadata | | Scope | Specific paths | Global changes | | Batching | Automatic | Automatic | | Consolidation | Smart path-based | By change address | | Performance | Optimized for specific paths | Tracks all changes | | Use Case | Watch specific data, get values | Track all changes, get metadata |
Use Matcher when: You want the current data at specific paths Use OnChange when: You need change metadata (from/to values, method, etc.)
License
See the root LICENSE file for license information.
Related Packages
- @jucio.io/react - React integration
- @jucio.io/state - Core state management system
- @jucio.io/state/history - Undo/redo functionality
- @jucio.io/state/on-change - Global change listeners
