This guide helps you understand Doh's USE (Unified State Engine) system, a comprehensive reactive state management solution that provides deep observation capabilities, automatic proxy generation, and seamless integrations with URL parameters, localStorage, and dynamic pattern instantiation.
The USE system features a revolutionary layered backend architecture with:
This guide covers:
Doh.use_state
Doh.observe_use
Doh.use_state_pattern
Doh.use_url_query
Doh.use_local_storage
Doh.use_snapshot
The USE system maintains a single, global state object (Doh._use_state
) accessed through a sophisticated layered backend architecture. The system creates stable proxies that maintain consistent identity while delegating to dynamically updated targets, enabling deep reactivity with intelligent change detection.
Immutable Proxy Layer (_use_state_backend.proxies
): A Map storing stable proxy references by path. Once a proxy is created for a path, it never changes, ensuring consistent object identity.
Target Management Layer (_use_state_backend.targets
): A WeakMap that associates each proxy with its current and previous target data, enabling seamless target swapping and change detection.
Observer Notification Layer (_use_state_backend.observers
): A Map organizing observers by path with efficient batching via microtasks and comprehensive path bubbling.
The USE system wraps all native array methods to provide change detection and observer notifications while maintaining stable proxy identity. This creates a specific behavior difference from standard JavaScript arrays:
Mutating Methods (return underlying target, not proxy):
sort()
, reverse()
, fill()
, copyWithin()
- Array is correctly modified, but returned value ≠ proxypush()
, pop()
, shift()
, unshift()
, splice()
- Operate correctly and trigger observersNon-Mutating Methods (work normally):
map()
, filter()
, slice()
, concat()
, join()
, toString()
- Return new values, no proxy involvementconst state = Doh.use_state({ numbers: [3, 1, 2] });
// ✅ Array IS sorted, observers ARE notified, but reference differs
const sorted = state.numbers.sort();
console.log(state.numbers); // [1, 2, 3] ✅ Correctly sorted
console.log(sorted === state.numbers); // false ⚠️ Different references
console.log(sorted); // [1, 2, 3] ✅ Still array-like
// ✅ This is the expected USE system behavior for stable proxy identity
The Doh.use_state
function provides access to the singleton state object, optionally melding in default values. It returns the root proxy from the layered backend architecture.
const state = Doh.use_state(defaults);
defaults
- (Optional) Object containing default values to meld into the state. Only keys that don't already exist or are undefined will be added.Returns the singleton state proxy object. All calls to use_state
return the same proxy instance, ensuring shared state across your entire application.
// First module initializes state with defaults
const state1 = Doh.use_state({
theme: 'light',
user: 'anonymous',
version: '1.0.0'
});
console.log(state1.theme); // 'light'
// Second module adds more defaults without overriding existing values
const state2 = Doh.use_state({
theme: 'dark', // Won't override existing 'light'
debug: false, // Will be added (new key)
apiUrl: '/api/v1' // Will be added (new key)
});
console.log(state1 === state2); // true (same singleton)
console.log(state2.theme); // 'light' (preserved from first call)
console.log(state2.debug); // false (added from second call)
// Direct property access and modification
state1.theme = 'dark';
console.log(state2.theme); // 'dark' (same object)
// Nested object creation with automatic proxy wrapping
state1.user = { profile: { name: 'John', settings: { theme: 'blue' } } };
console.log(state1.user.profile.name); // 'John' - all levels are reactive proxies
The Doh.observe_use
function provides comprehensive deep observation of state changes with intelligent path bubbling and efficient notification batching. It leverages the new backend architecture for optimal performance.
const removeObserver = Doh.observe_use(paths, callback, onDelete);
paths
- String or array of strings specifying which paths to observe (supports dot notation for nested properties)callback
- Function called for any change under the observed path(s). Receives a destructured object with change detailsonDelete
- (Optional) Function called when properties are deleted (set to undefined
). Receives similar destructured objectBoth callbacks receive a destructured object with these properties:
Main Callback Object:
fullPath
- Full dot-notation path where the change occurred (e.g., 'user.profile.name'
)prop
- The specific property that changed (e.g., 'name'
)newValue
- The new value being assignedoldValue
- The previous valueisNew
- Boolean indicating if this property is being created for the first timewatchPath
- The path you're watching that caught this change (e.g., 'user'
)Delete Callback Object:
fullPath
- Full dot-notation path where the deletion occurredprop
- The specific property that was deletedoldValue
- The value that was deletedwatchPath
- The path you're watching that caught this changeReturns a function that removes all observers when called.
The new backend architecture supports comprehensive path bubbling. When you observe a path, you receive notifications for all changes under that path:
const state = Doh.use_state({
user: {
profile: {
name: 'John',
settings: { theme: 'light' }
}
}
});
// Watch the user object - catches ALL changes under user.*
const userObserver = Doh.observe_use('user', ({ fullPath, prop, newValue, oldValue, isNew, watchPath }) => {
console.log(`Change detected: ${fullPath} | ${prop}: ${oldValue} → ${newValue} | via ${watchPath}`);
});
state.user.profile.name = 'Jane'; // Triggers: "user.profile.name | name: John → Jane | via user"
state.user.profile.settings.theme = 'dark'; // Triggers: "user.profile.settings.theme | theme: light → dark | via user"
state.user.profile.age = 25; // Triggers: "user.profile.age | age: undefined → 25 | via user" (isNew=true)
The system includes sophisticated notification batching:
const state = Doh.use_state({});
let notifications = [];
const observer = Doh.observe_use('', ({ fullPath, prop, newValue }) => {
notifications.push({ fullPath, prop, newValue });
});
// Multiple synchronous changes are batched
state.a = 1;
state.b = 2;
state.c = 3;
console.log(notifications.length); // 0 (notifications are batched)
// Wait for microtask to process batched notifications
await Doh.macrotask();
console.log(notifications.length); // 3 (all processed together)
The USE system includes sophisticated array diffing for precise change detection:
const state = Doh.use_state({
todos: [
{ id: 1, text: 'Task 1', completed: false },
{ id: 2, text: 'Task 2', completed: true }
]
});
// Watch the todos array
const todoObserver = Doh.observe_use('todos', ({ fullPath, prop, newValue, oldValue, isNew }) => {
console.log(`Todo change: ${fullPath} | ${prop}: ${oldValue} → ${newValue} | new: ${isNew}`);
});
// All of these trigger the observer with precise notifications:
state.todos[0].completed = true; // "todos.0.completed | completed: false → true"
state.todos[0].priority = 'high'; // "todos.0.priority | undefined → high" (isNew=true)
state.todos.push({ id: 3, text: 'New Task' }); // "todos.2 | 2: undefined → {id:3,...}" (new array item)
// Array method support with proper binding
const popped = state.todos.pop(); // Works correctly, maintains proxy context
// ⚠️ IMPORTANT: Stable Proxy References vs Array Method Return Values
// In the USE system, array methods work correctly but maintain stable proxy identity.
// Mutating methods that normally return 'this' will return the underlying target instead.
// Methods that return the underlying array (not the proxy):
const sorted = state.todos.sort(); // ✅ state.todos IS sorted, but sorted !== state.todos
const reversed = state.todos.reverse(); // ✅ state.todos IS reversed, but reversed !== state.todos
const filled = state.todos.fill(0); // ✅ state.todos IS filled, but filled !== state.todos
const spliced = state.todos.splice(0, 1); // ✅ state.todos IS modified, but spliced is removed elements array
// The array itself is correctly modified - only the reference equality differs:
console.log(state.todos[0]); // ✅ Shows sorted/reversed/filled result as expected
console.log(Array.isArray(sorted)); // ✅ true - returned value is still array-like
// Methods that return new arrays work normally (no reference issues):
const mapped = state.todos.map(x => x.id); // Returns new array, no proxy involved
const filtered = state.todos.filter(x => x.completed); // Returns new array, no proxy involved
You can observe multiple paths simultaneously:
// Watch multiple specific paths
const multiObserver = Doh.observe_use(['user.profile.name', 'todos'],
({ fullPath, prop, newValue, oldValue, isNew, watchPath }) => {
console.log(`[${watchPath}] detected change in ${fullPath}`);
}
);
state.user.profile.name = 'Alice'; // Triggers via "user.profile.name"
state.user.profile.email = 'test@test'; // Does NOT trigger (not watching user.profile.email)
state.todos[0].text = 'Updated'; // Triggers via "todos"
Properties can be deleted in two ways, both trigger the optional onDelete
callback:
const state = Doh.use_state({
user: { name: 'John', email: 'john@example.com', temp: 'data', cache: 'old' }
});
// Watch for changes and deletions
const observer = Doh.observe_use('user',
({ fullPath, prop, newValue, oldValue }) => {
console.log(`Changed: ${fullPath} - ${oldValue} → ${newValue}`);
},
({ fullPath, prop, oldValue }) => {
console.log(`Deleted: ${fullPath} - removed ${oldValue}`);
}
);
state.user.name = 'Jane'; // Triggers change callback
state.user.temp = undefined; // Triggers delete callback (set to undefined)
delete state.user.cache; // Triggers delete callback (delete keyword)
state.user.missing = null; // Triggers change callback (null is "unset", not deleted)
The Doh.use_state_pattern
function instantiates Doh patterns from state properties based on their value types. It handles strings as pattern names, arrays of mixed strings/objects, and objects with .pattern
keys. Objects without .pattern
keys generate warnings but continue processing. This enables declarative UI development where state structure directly drives pattern instantiation.
const instances = await Doh.use_state_pattern(...stateNames);
...stateNames
- State property names (strings) or arrays of names to processReturns a Promise that resolves to:
The system supports multiple formats for pattern definitions:
String Values: Treated as pattern names
state.myWidget = 'ButtonWidget';
const widget = await Doh.use_state_pattern('myWidget');
// Equivalent to: AsyncNew('ButtonWidget')
Object Values: Must contain .pattern
property
state.myButton = {
pattern: 'html',
tag: 'button',
html: 'Click me',
attr: { class: 'primary' }
};
const button = await Doh.use_state_pattern('myButton');
// Equivalent to: AsyncNew('html', { tag: 'button', html: 'Click me', ... })
Array Values: Arrays of strings and/or objects with .pattern
state.todoItems = [
{ pattern: 'html', tag: 'li', html: 'Task 1', id: 1 },
{ pattern: 'html', tag: 'li', html: 'Task 2', id: 2 },
'StandardListItem' // String patterns also supported
];
const items = await Doh.use_state_pattern('todoItems');
// Returns array of instantiated patterns
The system automatically tracks state changes and updates pattern instances accordingly:
const state = Doh.use_state({
todos: [
{ pattern: 'html', tag: 'li', html: 'Original Task', id: 1 }
]
});
// Initial instantiation
const initialTodos = await Doh.use_state_pattern('todos');
console.log(initialTodos.length); // 1
// Add new item - automatically creates new pattern instance
state.todos.push({ pattern: 'html', tag: 'li', html: 'New Task', id: 2 });
// Wait for observer processing
await Doh.macrotask();
// Cache is automatically updated with new instance
const cache = Doh._use_state_pattern_cache.get('todos');
console.log(cache.instances.length); // 2 - automatically updated
The system uses sophisticated array diffing to minimize unnecessary instantiation:
state.items = [
{ pattern: 'html', id: 1, html: 'Item 1' },
{ pattern: 'html', id: 2, html: 'Item 2' },
{ pattern: 'html', id: 3, html: 'Item 3' }
];
await Doh.use_state_pattern('items');
// Remove middle item - only destroys instance for removed item
state.items = state.items.filter(item => item.id !== 2);
// The system automatically:
// - Detects this as a removal operation at index 1
// - Destroys only the removed instance
// - Preserves instances for unchanged items
// - Updates the instances array efficiently
The system automatically manages the lifecycle of pattern instances:
// Automatic cleanup on replacement
state.widget = { pattern: 'OldWidget', config: 'old' };
await Doh.use_state_pattern('widget');
// Replace with new widget - automatically destroys old instance
state.widget = { pattern: 'NewWidget', config: 'new' };
// The system automatically calls appropriate cleanup:
// 1. instance.destroy() if available
// 2. instance.e.remove() for DOM elements
// 3. parentNode.removeChild() for DOM nodes
// 4. Safe fallback handling
You can process multiple state properties simultaneously:
state.header = { pattern: 'Header', title: 'My App' };
state.sidebar = [
{ pattern: 'MenuItem', label: 'Home' },
{ pattern: 'MenuItem', label: 'About' }
];
state.footer = 'StandardFooter';
// Process all at once
const [headerInstance, sidebarInstances, footerInstance] = await Doh.use_state_pattern([
'header', 'sidebar', 'footer'
]);
console.log(headerInstance.title); // 'My App'
console.log(sidebarInstances.length); // 2
console.log(footerInstance.pattern); // 'StandardFooter'
The system provides helpful warnings for malformed data:
state.badData = [
{ pattern: 'html', html: 'Good Item' },
{ html: 'Missing Pattern Key' }, // Warns but continues
{ pattern: 'html', html: 'Another Good Item' }
];
await Doh.use_state_pattern('badData');
// Console: [USE STATE PATTERN] Array item must be string or object with .pattern key: {html: "Missing Pattern Key"}
// Still processes the valid items
Perfect for building reactive UIs:
// Reactive todo list
const state = Doh.use_state({
todos: []
});
// Set up automatic pattern instantiation
await Doh.use_state_pattern('todos');
// Add new todos - UI automatically updates
function addTodo(text) {
const newTodo = {
pattern: 'TodoItem',
id: Date.now(),
text: text,
completed: false
};
state.todos = [...state.todos, newTodo];
}
// Remove todos - UI automatically updates
function removeTodo(id) {
state.todos = state.todos.filter(todo => todo.id !== id);
}
// Update todos - UI automatically updates
function toggleTodo(id) {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
The system maintains internal cache that you can inspect:
// After using use_state_pattern, inspect the cache
const cache = Doh._use_state_pattern_cache.get('myStateName');
console.log({
instances: cache.instances, // Array of instantiated patterns
sourceRef: cache.sourceRef, // Clean copy of source data
remover: cache.remover, // Observer cleanup function
destroyers: cache.destroyers // Instance cleanup functions
});
The USE system works naturally with Doh's MOC (Melded Object Composition) through independent tracks that handle state synchronization. Each USE track operates independently, so you can mix and match them based on your needs without requiring USE_STATE
as a prerequisite.
USE_STATE_DEFAULT
is a melder that provides state-backed default cascade. When a property uses this melder, its value becomes the default for the global state, and the property immediately connects via mimic_use
.
Pattern('StateWidget', {
moc: {
theme: 'USE_STATE_DEFAULT', // Immediate state connection
config: 'USE_STATE_DEFAULT'
},
theme: 'light', // Default for state.theme
config: { size: 'medium', enabled: true }
});
// Property connects to state immediately on first set
const widget = New('StateWidget');
// widget.theme ↔ state.theme (bidirectional sync established)
The USE tracks operate independently and can work together or separately. Each provides its own state synchronization without requiring other tracks.
Pattern('FlexibleWidget', {
moc: {
todos: ['Array', 'USE_STATE_PATTERN'], // Array + pattern instantiation
theme: ['Object', 'USE_LOCAL_STORAGE'], // Object + localStorage
search: 'USE_URL_QUERY', // Just URL sync
preferences: ['USE_STATE', 'USE_LOCAL_STORAGE'], // State + localStorage
complete: 'USE_STATE_DEFAULT' // Just state-backed defaults
}
});
MOC tracks are applied during object_phase
in this sequence:
mimic_use
connects to state on first setPattern('ComprehensiveExample', {
moc: {
items: ['Array', 'USE_LOCAL_STORAGE', 'USE_STATE_PATTERN']
},
items: [{ pattern: 'DefaultItem', text: 'starter' }]
});
// Execution flow:
// 1. Array melding processes default items with any inherited items
// 2. localStorage attempts to load previously saved items
// 3. Pattern instantiation enabled for the final array
// 4. mimic_use connects items to global state
// Result: Persistent, reactive array with automatic pattern instantiation
For array modifications, mutation is more performant than replacement:
Pattern('TodoList', {
moc: { todos: ['Array', 'USE_STATE_PATTERN'] },
// Preferred: Direct mutation
addTodo(text) {
this.todos.push({ pattern: 'TodoItem', text, id: Date.now() });
},
removeTodo(id) {
const index = this.todos.findIndex(t => t.id === id);
if (index >= 0) this.todos.splice(index, 1);
},
// Also works: Array replacement (less performant)
addTodoReplacement(text) {
this.todos = [...this.todos, { pattern: 'TodoItem', text, id: Date.now() }];
}
});
Simple localStorage theme:
Pattern('ThemeManager', {
moc: { theme: 'USE_LOCAL_STORAGE' },
theme: 'system' // localStorage default
});
URL-synchronized search with state:
Pattern('SearchInterface', {
moc: {
query: ['USE_URL_QUERY', 'USE_STATE'],
filters: 'USE_URL_QUERY'
},
query: '',
filters: { category: 'all' }
});
Full-featured reactive list:
Pattern('ReactiveList', {
moc: {
items: ['Array', 'USE_STATE_PATTERN', 'USE_LOCAL_STORAGE']
},
items: []
});
The Doh.use_url_query
function creates bidirectional synchronization between USE state properties and URL query parameters, enabling shareable and bookmarkable application state with automatic URL encoding/decoding.
const cleanup = Doh.use_url_query(...params);
Each parameter can be:
{ paramName: defaultValue, ... }
Returns a cleanup function that removes URL parameters but keeps state watching active.
const state = Doh.use_state({
searchTerm: '',
page: 1,
theme: 'light'
});
// Sync single parameter
Doh.use_url_query('searchTerm');
// State changes update URL
state.searchTerm = 'react'; // URL becomes: ?searchTerm=react
// URL parameters initialize state on page load
// If URL is ?searchTerm=vue, state.searchTerm becomes 'vue'
// Sync multiple parameters
Doh.use_url_query(['searchTerm', 'page', 'theme']);
// Sync with default values
const cleanup = Doh.use_url_query({
searchTerm: '', // Default empty string
page: 1, // Default page 1
showFilters: false, // Boolean values work
category: null // null is a valid value
});
// Complex objects are JSON-encoded
state.filters = { category: 'tech', active: true };
// URL: ?filters=%7B%22category%22%3A%22tech%22%2C%22active%22%3Atrue%7D
The system automatically handles value encoding and type preservation:
// All these values appear correctly in URL:
state.searchTerm = 'hello world'; // Encoded: ?searchTerm=hello%20world
state.page = 0; // Shows: ?page=0
state.enabled = false; // Shows: ?enabled=false
state.data = null; // Shows: ?data=null
state.config = { theme: 'dark' }; // Shows: ?config=%7B%22theme%22%3A%22dark%22%7D
// Only undefined hides from URL (but keeps watching):
state.searchTerm = undefined; // URL parameter removed, but still watched
state.searchTerm = 'back'; // URL parameter reappears: ?searchTerm=back
The system validates parameter names and values:
// Valid parameter names (alphanumeric, dash, underscore only)
Doh.use_url_query('search-term'); // ✓ Valid
Doh.use_url_query('page_number'); // ✓ Valid
Doh.use_url_query('user@email'); // ✗ Invalid - cleaned to 'useremail'
// Unsupported values are handled gracefully
state.invalidParam = Symbol('test'); // Won't appear in URL, shows warning
On page load, existing URL parameters initialize state:
// URL: ?searchTerm=javascript&page=3&showFilters=true
const state = Doh.use_state();
Doh.use_url_query(['searchTerm', 'page', 'showFilters']);
// After initialization:
console.log(state.searchTerm); // 'javascript' (parsed from URL)
console.log(state.page); // 3 (parsed as number)
console.log(state.showFilters); // true (parsed as boolean)
// The returned cleanup function only removes URL parameters, keeps watching state
const cleanup = Doh.use_url_query('searchTerm', 'page');
cleanup(); // Removes ?searchTerm&page from URL, but state changes still work
// To stop watching entirely, use use_remove_url_query
Doh.use_remove_url_query('searchTerm'); // Stops watching + removes URL
Doh.use_remove_url_query(['page', 'theme']); // Multiple parameters
The Doh.use_local_storage
function creates bidirectional synchronization between USE state properties and browser localStorage, enabling persistent storage across browser sessions with automatic serialization and type preservation.
const cleanup = Doh.use_local_storage(...params);
Each parameter can be:
{ paramName: defaultValue, ... }
Returns a cleanup function that removes localStorage entries and stops watching.
const state = Doh.use_state({
theme: 'light',
userPrefs: { lang: 'en' },
autoSave: false
});
// Sync single parameter
Doh.use_local_storage('theme');
// State changes save to localStorage
state.theme = 'dark'; // localStorage.setItem('theme', 'dark')
// On page reload, state.theme will be 'dark' (restored from localStorage)
// Sync multiple parameters
Doh.use_local_storage(['theme', 'userPrefs', 'autoSave']);
// Sync with default values
const cleanup = Doh.use_local_storage({
theme: 'light', // Default theme
userPrefs: { lang: 'en', notifications: true }, // Default object
recentItems: [], // Default array
lastVisited: null // Default null value
});
// Complex objects are JSON-serialized safely
state.userPrefs = { lang: 'es', notifications: false, fontSize: 14 };
// localStorage: {"lang":"es","notifications":false,"fontSize":14}
The system preserves value types correctly across browser sessions:
// All value types are preserved correctly:
state.theme = 'dark'; // String: stored as "dark"
state.fontSize = 16; // Number: stored as "16", parsed back to 16
state.enabled = false; // Boolean: stored as "false", parsed back to false
state.data = null; // Null: stored as "null", parsed back to null
state.config = { key: 'val' }; // Object: JSON serialized and parsed
// Only undefined removes from localStorage:
state.theme = undefined; // localStorage.removeItem('theme')
state.theme = 'light'; // Reappears in localStorage
The system includes safe serialization to handle circular references and Doh objects:
// Circular references are handled
const circular = { name: 'test' };
circular.self = circular;
state.circularData = circular; // Safely serialized with circular reference markers
// Doh objects are filtered out to avoid corruption
const dohInstance = New('html', { html: 'test' });
state.mixedData = {
text: 'safe data',
dohObj: dohInstance, // Will be filtered as '[Filtered Doh Object]'
numbers: [1, 2, 3] // Will be preserved
};
const state = Doh.use_state();
// Set up persistent storage
Doh.use_local_storage({
userSettings: {
theme: 'light',
language: 'en',
autoSave: true
},
workspaceLayout: 'default',
recentFiles: []
});
// User makes changes
state.userSettings.theme = 'dark';
state.recentFiles = ['file1.txt', 'file2.txt'];
// Page refresh - all values automatically restored from localStorage
// No additional code needed for persistence!
The system uses intelligent deep comparison to avoid unnecessary localStorage writes:
// Only actual changes trigger localStorage updates
state.config = { theme: 'dark', fontSize: 14 };
// This won't trigger localStorage update (same value)
state.config = { theme: 'dark', fontSize: 14 };
// This will trigger update (different value)
state.config = { theme: 'dark', fontSize: 16 };
// Automatically handles environment availability
Doh.use_local_storage('userPrefs'); // Works in browser
// In Node.js environments:
// [USE STORAGE] localStorage not available - gracefully ignored
// Storage quota handling:
state.largeData = { /* huge object */ };
// [USE STORAGE] Error setting localStorage value: QuotaExceededError
// The returned cleanup removes localStorage entries AND stops observers
const cleanup = Doh.use_local_storage('theme', 'userPrefs');
cleanup(); // Removes 'theme' and 'userPrefs' from localStorage + stops watching
// To stop watching entirely, use use_remove_local_storage
Doh.use_remove_local_storage('theme'); // Stops watching + removes from localStorage
Doh.use_remove_local_storage(['prefs', 'cache']); // Multiple parameters
The USE system includes an extensive snapshot system for capturing and replaying state changes with sophisticated trigger logic, timeline management, and persistent storage options. This provides powerful capabilities like undo/redo systems, session restoration, and state debugging.
The snapshot system supports:
save_as
// Set up automatic document snapshots with timeline storage
await Doh.use_snapshot('document_history', {
triggers: { 'document.content': '*' }, // Any content change
states: ['document.content', 'document.title'],
in_timeline: 50 // Keep last 50 in IndexedDB
});
// Use timeline navigation for undo/redo
await Doh.use_timeline_undo('document_history'); // Undo last change
await Doh.use_timeline_redo('document_history'); // Redo last change
📖 For complete snapshot documentation, see use_snapshot.md
The snapshot system is powerful enough to warrant its own comprehensive documentation covering all trigger types, storage options, timeline management, and advanced patterns.
The USE system provides several specialized observer functions for different use cases:
Monitors when new properties are added to the state:
const removeNewObserver = Doh.observe_new_use((propName, value, fullPath) => {
console.log(`New property detected: ${propName} = ${value} at ${fullPath}`);
// Plugin logic based on property patterns
if (propName.includes('_ui')) {
createUIControl(propName, value);
} else if (propName.includes('_config')) {
updateConfiguration(propName, value);
}
});
state.new_ui_widget = 'button'; // Triggers: "New property detected: new_ui_widget = button"
state.user_config_theme = 'dark'; // Triggers: "New property detected: user_config_theme = dark"
Provides comprehensive monitoring of all root-level state changes:
const removeAllObserver = Doh.observe_all_use((propName, newValue, oldValue, isNewProperty) => {
const operation = isNewProperty ? 'CREATE' : 'UPDATE';
console.log(`Storage: ${operation} ${propName} - ${oldValue} → ${newValue}`);
// Don't persist temporary values
if (!propName.startsWith('_temp')) {
saveToStorage(propName, newValue);
}
});
state.count = 5; // Storage: UPDATE count - 0 → 5
state.newProperty = 'hello'; // Storage: CREATE newProperty - undefined → hello
state._temp_calc = 42; // Storage: CREATE _temp_calc... (but not saved)
Creates bidirectional synchronization between external objects and USE state:
const state = Doh.use_state({ theme: 'light' });
const uiComponent = { currentTheme: 'initial' };
// Bidirectional sync: uiComponent.currentTheme ↔ state.theme
const removeMimic = Doh.mimic_use(uiComponent, 'currentTheme', 'theme',
(newVal, oldVal) => console.log(`Theme synced: ${oldVal} → ${newVal}`)
);
console.log(uiComponent.currentTheme); // 'light' (synced from state)
// Change via state - updates external object
state.theme = 'dark';
console.log(uiComponent.currentTheme); // 'dark'
// Change via external object - updates state
uiComponent.currentTheme = 'blue';
console.log(state.theme); // 'blue'
removeMimic();
The USE system includes comprehensive debugging and monitoring capabilities:
// Enable debug logging for all proxy operations
Doh.DebugMode = true;
// AND
Doh.enableUseDebug(true);
// All proxy operations will now be logged:
// [USE DEBUG] proxy_create at user.profile: {new: true}
// [USE DEBUG] proxy_get at user.profile.name: {hasValue: true, valueType: 'string'}
// [USE DEBUG] proxy_set at user.profile.name: {isNew: false, oldValue: 'string', newValue: 'string'}
state.user = { profile: { name: 'John' } };
state.user.profile.name = 'Jane';
// Disable debug logging
Doh.DebugMode = false;
Doh.enableUseDebug(false);
// Get history of all debug operations
const history = Doh.getUseDebugHistory();
console.log('Debug operations:', history);
// Clear debug history
Doh.clearUseDebugHistory();
// Inspect the layered backend architecture
const backend = Doh._use_state_backend;
console.log({
proxyCount: backend.proxies.size, // Number of stable proxies
observerPaths: backend.observers.size, // Number of observed paths
pendingNotifications: backend.pendingNotifications.size, // Queued notifications
debugEnabled: backend.debug.enabled // Debug status
});
// Inspect specific proxy relationships
backend.proxies.forEach((proxy, path) => {
const target = backend.targets.get(proxy);
console.log(`Proxy ${path}:`, {
hasTarget: !!target,
currentType: typeof target?.current
});
});
Pattern('StoragePlugin', {
object_phase: function() {
this.observer = Doh.observe_all_use((prop, newVal, oldVal, isNew) => {
if (!prop.startsWith('_temp')) {
localStorage.setItem(`app_${prop}`, JSON.stringify(newVal));
}
});
}
});
// Plugin automatically persists all state changes
const plugin = New('StoragePlugin');
const state = Doh.use_state({ setting: 'value' });
state.userTheme = 'dark'; // Automatically saved to localStorage
The USE system is designed for high performance with several key optimizations:
Initialize State Early: Set up your state structure early in your application lifecycle using default melding.
Use Descriptive Property Names: Leverage naming patterns for plugin systems (e.g., module_action_target
).
Always Clean Up: Store and call observer remover functions to prevent memory leaks:
const cleanup = [];
cleanup.push(Doh.observe_use('path', callback));
cleanup.push(Doh.use_url_query('param'));
cleanup.push(Doh.use_local_storage('data'));
// Later...
cleanup.forEach(fn => fn());
Avoid Observer Loops: Be mindful of observer chains that might create circular updates:
// Potential loop - be careful
Doh.observe_use('a', () => state.b = 'updated');
Doh.observe_use('b', () => state.a = 'updated');
Use Prefixes for Control: Use prefixes like _temp
or _private
for properties that shouldn't trigger certain plugins.
Leverage State Normalization: Keep your state structure flat when possible to maximize plugin effectiveness.
Debug Complex Flows: Use debug mode to understand complex observation patterns:
Doh.enableUseDebug(true);
// ... perform operations ...
const history = Doh.getUseDebugHistory();
Doh.enableUseDebug(false);
Batch State Updates: When making multiple changes, batch them to minimize observer notifications:
// Good: Single notification batch
state.user = { ...state.user, name: 'John', email: 'john@test.com' };
// Less efficient: Multiple notifications
state.user.name = 'John';
state.user.email = 'john@test.com';
Array Method Reference Behavior: Understand that array methods work correctly but don't return proxy references:
// ⚠️ The operation works, but reference equality differs
const sorted = state.items.sort((a, b) => a.name.localeCompare(b.name));
console.log(state.items[0].name); // ✅ Correctly shows first sorted item
console.log(sorted === state.items); // ❌ false - sorted is the target, not proxy
// ✅ Correct way to check if operation worked:
state.numbers = [3, 1, 2];
state.numbers.sort();
console.log(state.numbers); // ✅ [1, 2, 3] - array is correctly sorted
// ✅ Methods affected: sort(), reverse(), fill(), copyWithin()
// ✅ Methods NOT affected: map(), filter(), slice(), concat() (return new arrays)
The USE system provides comprehensive cleanup utilities:
const observer1 = Doh.observe_use('prop1', callback1);
const observer2 = Doh.observe_new_use(callback2);
const mimic1 = Doh.mimic_use(obj, 'prop', 'stateProp');
const urlSync = Doh.use_url_query('param');
const storageSync = Doh.use_local_storage('data');
// Remove individual resources
observer1();
observer2();
mimic1();
urlSync();
storageSync();
// Remove all USE observers
Doh.clear_use_observers();
// Clear pattern cache
Doh._use_state_pattern_cache.clear();
// Remove all URL query syncs
Doh.use_remove_url_query(['param1', 'param2']);
// Remove all localStorage syncs
Doh.use_remove_local_storage(['data1', 'data2']);
USE state changes and observer propagation occur on the microtask queue. If you set a state value and need to immediately read a mirrored/derived value in the same tick (especially in tests), flush with:
// Set state values
state.searchTerm = 'react';
state.page = 2;
// Wait for observers and syncs to complete with a timeout
await Doh.macrotask();
// or use the Doh utility for microqueue waiting:
await Doh.microtask();
// Now all observers, URL updates, and localStorage syncs have completed
console.log(new URL(window.location).searchParams.get('searchTerm')); // 'react'
console.log(localStorage.getItem('page')); // '2'
The USE system represents the evolution of reactive state management in Doh.js, providing a powerful foundation for building sophisticated, observable applications while maintaining compatibility with existing Doh patterns and infrastructure.