@jucie-state/matcher
v1.0.8
Published
Matcher plugin for @jucie-state/core - path-based change tracking
Maintainers
Readme
@jucie-state/matcher
Path-based change tracking plugin for @jucie-state/core that allows you to watch specific paths in your state tree and react to changes with automatic batching and smart consolidation.
Features
- 🎯 Path Matching: Watch specific paths in your state tree
- 🌳 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-state/core
Installation
npm install @jucie-state/matcherNote: Requires @jucie-state/core as a peer dependency.
Quick Start
import { createState } from '@jucie-state/core';
import { Matcher, createMatcher } from '@jucie-state/matcher';
// Create a matcher
const userMatcher = createMatcher(['user'], (changes) => {
console.log('User changed:', changes);
});
// 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 fires
state.set(['user', 'name'], 'Bob');
// Console: "User changed: { name: 'Bob', age: 30 }"
// Change settings - matcher doesn't fire
state.set(['settings', 'theme'], 'light');API Reference
Creating Matchers
createMatcher(path, handler)
Create a matcher that watches a specific path in the state tree.
import { createMatcher } from '@jucie-state/matcher';
const matcher = createMatcher(['users', 'profile'], (changes) => {
console.log('Profile changed:', changes);
});Parameters:
path(Array): Path to watch (e.g.,['user'],['users', 'profile'])handler(Function): Callback function that receives the changed value(s)
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 '@jucie-state/core';
import { Matcher } from '@jucie-state/matcher';
const state = createState({ user: { name: 'Alice' } });
state.install(Matcher);
const unsubscribe = state.matcher.createMatcher(['user'], (changes) => {
console.log('User changed:', changes);
});
// Later: remove the matcher
unsubscribe();Returns: Unsubscribe function
state.matcher.addMatcher(matcher)
Add an existing matcher to the plugin.
const matcher = createMatcher(['user'], (changes) => {
console.log('User:', changes);
});
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:
Exact Match
Watches the exact path specified:
const matcher = createMatcher(['user', 'profile'], (changes) => {
console.log('Exact profile change:', changes);
});
state.set(['user', 'profile'], { bio: 'Hello' }); // ✅ Fires
state.set(['user', 'profile', 'bio'], 'Hi'); // ✅ Fires (parent changed)
state.set(['user'], { profile: { bio: 'Hi' } }); // ✅ Fires (child changed)
state.set(['user', 'settings'], {}); // ❌ Doesn't fireParent Match
Fires when a parent path changes:
const matcher = createMatcher(['user'], (changes) => {
console.log('User or descendants changed:', changes);
});
state.set(['user', 'name'], 'Alice'); // ✅ Fires
state.set(['user', 'profile', 'bio'], 'Hello'); // ✅ Fires
state.set(['user'], { name: 'Bob' }); // ✅ FiresChild Match
When a parent path is matched, child changes are consolidated:
const matcher = createMatcher(['users'], (changes) => {
console.log('Users changed:', changes);
});
state.set(['users', 'alice'], { name: 'Alice' });
state.set(['users', 'bob'], { name: 'Bob' });
// Both changes are batched and consolidated:
// Console: "Users changed: { alice: { name: 'Alice' }, bob: { name: 'Bob' } }"Configuration
Initialize with Matchers
import { createState } from '@jucie-state/core';
import { Matcher, createMatcher } from '@jucie-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 '@jucie-state/core';
import { Matcher } from '@jucie-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'], (user) => {
console.log('User changed:', user);
});
const validator = createMatcher(['user'], (user) => {
if (!user.email) {
console.warn('User has no email!');
}
});
state.matcher.addMatcher(logger);
state.matcher.addMatcher(validator);
state.set(['user'], { name: 'Alice' });
// Both matchers fireDynamic Matcher Management
// Add matcher conditionally
if (process.env.NODE_ENV === 'development') {
const debugMatcher = state.matcher.createMatcher(['*'], (changes) => {
console.log('DEBUG: State changed:', changes);
});
}
// 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'], (user) => {
console.log('Entire user object:', user);
});
const profileMatcher = createMatcher(['user', 'profile'], (profile) => {
console.log('Just profile:', profile);
});
const nameMatcher = createMatcher(['user', 'profile', 'name'], (name) => {
console.log('Just name:', name);
});
state.set(['user', 'profile', 'name'], 'Alice');
// All three matchers fire with their respective scopesBatching and Consolidation
const matcher = createMatcher(['items'], (items) => {
console.log('Items changed:', Object.keys(items));
});
// 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 all changes:
// Console: "Items changed: ['item1', 'item2', 'item3']"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 (user) => {
try {
await fetch('/api/user', {
method: 'PUT',
body: JSON.stringify(user)
});
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 | |---------|---------|----------| | Scope | Specific paths | Global changes | | Batching | Automatic | Manual | | Consolidation | Smart path-based | No consolidation | | Performance | Optimized for specific paths | All changes | | Use Case | Watch specific data | Track all changes |
Use Matcher when you want to watch specific parts of your state tree. Use OnChange when you need to track all state changes.
License
See the root LICENSE file for license information.
Related Packages
- @jucie-reactive/core - Reactive library
- @jucie-reactive/jucie-state - Plugin for adding reactivity to Jucie State
- @jucie-state/core - Core state management system
- @jucie-state/history - Undo/redo functionality
- @jucie-state/on-change - Global change listeners
