DevTools, Real-World Examples, and API Reference
Part 1: DevTools
What Are DevTools?
The DevTools provide visibility into the reactive system — you can track states, log changes, and inspect history. They auto-enable on localhost and 127.0.0.1 so you get debugging tools in development without any setup.
Auto-Enabling
// DevTools auto-enable if you're on localhost:
if (window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1') {
DevTools.enable();
// Console: [DevTools] Enabled - inspect with window.__REACTIVE_DEVTOOLS__
}To manually enable or disable:
ReactiveUtils.DevTools.enable(); // Enable
ReactiveUtils.DevTools.disable(); // DisableTracking States
Register a state with a name so it shows up in DevTools:
const appState = state({ count: 0 });
ReactiveUtils.DevTools.trackState(appState, 'App State');
const userState = state({ name: 'Alice' });
ReactiveUtils.DevTools.trackState(userState, 'User');Logging Changes
Log state changes for debugging:
ReactiveUtils.DevTools.logChange(appState, 'count', 0, 5);
// Records: { stateId: 1, stateName: 'App State', key: 'count',
// oldValue: 0, newValue: 5, timestamp: ... }Viewing Tracked States
const states = ReactiveUtils.DevTools.getStates();
console.table(states);
// [
// { id: 1, name: 'App State', created: ..., updates: [...], state: Proxy },
// { id: 2, name: 'User', created: ..., updates: [...], state: Proxy }
// ]Viewing Change History
const history = ReactiveUtils.DevTools.getHistory();
console.table(history);
// [
// { stateId: 1, stateName: 'App State', key: 'count',
// oldValue: 0, newValue: 5, timestamp: ... },
// ...
// ]History is capped at 50 entries by default (configurable via DevTools.maxHistory).
Clearing History
ReactiveUtils.DevTools.clearHistory();
// Clears both global history and per-state update logsInspecting from the Console
Because DevTools are exposed on window.__REACTIVE_DEVTOOLS__, you can inspect everything from the browser console:
// In the browser console:
__REACTIVE_DEVTOOLS__.getStates() // All tracked states
__REACTIVE_DEVTOOLS__.getHistory() // Change history
__REACTIVE_DEVTOOLS__.clearHistory() // Clear all logsDevTools API Summary
| Method | Description |
|---|---|
.enable() | Enable DevTools, expose on window.__REACTIVE_DEVTOOLS__ |
.disable() | Disable DevTools, remove from window |
.trackState(state, name) | Register a state for tracking |
.trackEffect(effect, name) | Register an effect for tracking |
.logChange(state, key, old, new) | Log a state change |
.getStates() | Get all tracked states |
.getHistory() | Get change history (max 50 entries) |
.clearHistory() | Clear all history |
Part 2: Real-World Examples
Example 1: Search with Debounced Fetch
const state = state({ query: '' });
const results = asyncState([]);
// Async effect that re-runs whenever query changes
const stop = asyncEffect(async (signal) => {
const query = state.query;
if (!query) return;
// Wait 300ms before fetching (debounce)
await new Promise(resolve => setTimeout(resolve, 300));
// If aborted during the wait, stop here
if (signal.aborted) return;
const response = await fetch(`/api/search?q=${query}`, { signal });
const data = await response.json();
results.data = data;
});
// UI updates reactively
effect(() => {
Elements.results.update({ innerHTML: results.loading });
? '<p>Searching...</p>'
: (results.data || []).map(r => `<div>${r.name}</div>`).join('');
});Key patterns: asyncEffect with signal, debounce via setTimeout, AbortSignal cancels both the timeout and the fetch
Example 2: Dashboard with Safe Effects
const dashboard = state({
metrics: new Map(),
alerts: new Set()
});
// Track for DevTools
ReactiveUtils.DevTools.trackState(dashboard, 'Dashboard');
// Safe effect — won't crash if the DOM element is missing
safeEffect(() => {
Elements.metricCount.update({ textContent: dashboard.metrics.size });
}, {
errorBoundary: {
onError: (err) => console.warn('Dashboard render failed:', err.message),
fallback: () => { /* silently continue */ }
}
});
// Reactive Map — effects trigger on mutations
dashboard.metrics.set('cpu', 45);
dashboard.metrics.set('memory', 72);
// Reactive Set — effects trigger on add/delete
dashboard.alerts.add('High CPU usage');Key patterns: Reactive Map/Set, safeEffect for resilient UI, DevTools tracking
Example 3: Data Table with Computed and Caching
const table = state({
rows: [],
sortBy: 'name',
filterText: ''
});
// Computed properties — cached, only recalculate when dependencies change
computed(table, {
filteredRows: function() {
if (!this.filterText) return this.rows;
const text = this.filterText.toLowerCase();
return this.rows.filter(r =>
Object.values(r).some(v => String(v).toLowerCase().includes(text))
);
}
});
computed(table, { sortedRows: function() {
const field = this.sortBy;
return [...this.filteredRows].sort((a, b) =>
String(a[field]).localeCompare(String(b[field]))
);
} });
computed(table, {
rowCount: function() {
return this.filteredRows.length;
}
});
// Multiple effects read sortedRows — computed only runs once per tick
effect(() => {
renderTable(table.sortedRows);
});
effect(() => {
Elements.count.update({ textContent: `${table.rowCount} of ${table.rows.length} rows` });
});Key patterns: Chained computed properties (filteredRows → sortedRows), caching prevents redundant calculations
Example 4: User Profile with Async State
const profile = asyncState(null, {
onSuccess: (data) => console.log('Profile loaded:', data.name),
onError: (error) => console.error('Failed to load profile')
});
// Load profile
async function loadProfile(userId) {
await profile.execute(async (signal) => {
const response = await fetch(`/api/users/${userId}`, { signal });
if (!response.ok) throw new Error('User not found');
return response.json();
});
}
// Reactive UI
effect(() => {
let html;
if (profile.isIdle) html = '<p>Select a user</p>';
else if (profile.loading) html = '<p>Loading profile...</p>';
else if (profile.isError) html = `<p>Error: ${profile.error.message}</p><button onclick="profile.refetch()">Retry</button>`;
else if (profile.isSuccess) html = `<h2>${profile.data.name}</h2><p>${profile.data.email}</p>`;
Elements.profile.update({ innerHTML: html });
});
// Load user 1, then quickly switch to user 2
loadProfile(1);
loadProfile(2); // User 1 request is aborted, user 2 loadsKey patterns: asyncState with computed status properties, refetch for retry, race condition prevention
Part 3: Complete API Reference
ReactiveEnhancements (Global)
| Property/Method | Description |
|---|---|
.batch(fn) | Enhanced batch function |
.queueUpdate(fn, priority) | Queue a function at a specific priority |
.safeEffect(fn, opts) | Effect with error boundary |
.safeWatch(state, key, fn, opts) | Watcher with error boundary |
.asyncEffect(fn, opts) | Async effect with AbortSignal |
.asyncState(initial, opts) | Async state with race prevention |
.ErrorBoundary | Error boundary class |
.DevTools | Development tools |
.PRIORITY | Priority constants: { COMPUTED: 1, WATCH: 2, EFFECT: 3 } |
Also Available on ReactiveUtils
| Method | Description |
|---|---|
safeEffect(fn, opts) | Effect with error boundary |
safeWatch(state, key, fn, opts) | Watcher with error boundary |
asyncEffect(fn, opts) | Async effect with AbortSignal |
asyncState(initial, opts) | Async state |
ReactiveUtils.ErrorBoundary | Error boundary class |
ReactiveUtils.DevTools | Development tools |
Transparent Patches
| What changed | Enhancement |
|---|---|
state() | Wraps Map/Set in reactive proxies |
computed(state, { key: fn }) | Caching + circular dependency detection |
asyncEffect(fn, options?)
| Parameter | Type | Description |
|---|---|---|
fn | async (signal) => {} | Async function receiving AbortSignal |
options.onError | Function | Called for non-abort errors |
| Returns | Function | Dispose function (stops effect + aborts) |
asyncState(initialValue, options?)
| Parameter | Type | Description |
|---|---|---|
initialValue | any | Initial data value |
options.onSuccess | Function | Called on successful fetch |
options.onError | Function | Called on failed fetch |
State properties:
| Property | Type | Description |
|---|---|---|
.data | any | Fetched data |
.loading | Boolean | Request in progress |
.error | Error/null | Last error |
.requestId | Number | Current request counter |
.isSuccess | Boolean (computed) | Data loaded, no error |
.isError | Boolean (computed) | Has error, not loading |
.isIdle | Boolean (computed) | No data, no error, not loading |
Methods:
| Method | Returns | Description |
|---|---|---|
.execute(fn) | Promise | Run async function with cancellation |
.abort() | — | Cancel current request |
.reset() | — | Reset to initial state (also aborts) |
.refetch() | Promise | Re-run the last function |
execute return values:
| Result | Shape |
|---|---|
| Success | { success: true, data: ... } |
| Validation failed (stale) | { success: false, stale: true } |
| Aborted | { success: false, aborted: true } |
| Error | { success: false, error: Error } |
safeEffect(fn, options?)
| Parameter | Type | Description |
|---|---|---|
fn | Function | The effect function |
options.errorBoundary | Object | ErrorBoundary options |
| Returns | Function | Dispose function |
safeWatch(state, keyOrFn, callback, options?)
| Parameter | Type | Description |
|---|---|---|
state | Reactive state | State to watch |
keyOrFn | String/Function | Property key or getter |
callback | Function | Called when value changes |
options.errorBoundary | Object | ErrorBoundary options |
| Returns | Function | Dispose function |
ErrorBoundary
new ReactiveUtils.ErrorBoundary(options)| Option | Type | Default | Description |
|---|---|---|---|
onError | Function | console.error | Error handler |
fallback | Function | undefined | Fallback value provider |
retry | Boolean | true | Whether to retry |
maxRetries | Number | 3 | Max retry attempts |
retryDelay | Number | 0 | Delay between retries (ms) |
Methods:
| Method | Description |
|---|---|
.wrap(fn, context) | Returns a wrapped function with error handling |
DevTools
| Method | Description |
|---|---|
.enable() | Enable and expose on window.__REACTIVE_DEVTOOLS__ |
.disable() | Disable and remove from window |
.trackState(state, name) | Register a state |
.trackEffect(effect, name) | Register an effect |
.logChange(state, key, old, new) | Record a change |
.getStates() | Get all tracked states |
.getHistory() | Get change history |
.clearHistory() | Clear all history |
| Property | Default | Description |
|---|---|---|
.enabled | false (auto on localhost) | Whether DevTools are active |
.maxHistory | 50 | Max history entries |
Load Order
<!-- 1. Reactive Core (required) -->
<script type="module">
import { load } from 'https://cdn.jsdelivr.net/npm/dom-helpers-js@2.10.0/dist/dom-helpers.loader.esm.min.js';
await load('reactive');
</script>
<!-- 2. Array Patch (recommended) -->
<script type="module">
import { load } from 'https://cdn.jsdelivr.net/npm/dom-helpers-js@2.10.0/dist/dom-helpers.loader.esm.min.js';
await load('reactive');
</script>
<!-- 3. Collections, Forms (optional) -->
<script type="module">
import { load } from 'https://cdn.jsdelivr.net/npm/dom-helpers-js@2.10.0/dist/dom-helpers.loader.esm.min.js';
await load('reactive');
</script>
<!-- 4. Cleanup System (recommended, load before enhancements) -->
<script type="module">
import { load } from 'https://cdn.jsdelivr.net/npm/dom-helpers-js@2.10.0/dist/dom-helpers.loader.esm.min.js';
await load('reactive');
</script>
<!-- 5. Enhancements (load LAST) -->
<script type="module">
import { load } from 'https://cdn.jsdelivr.net/npm/dom-helpers-js@2.10.0/dist/dom-helpers.loader.esm.min.js';
await load('reactive');
</script>Order matters: Enhancements patch state() and computed, so they must load after everything else.
Congratulations!
You've completed the Reactive Enhancements learning path. You now understand:
- ✅ Priority-based batching with infinite loop detection
- ✅ Deep reactivity for Map and Set
- ✅ Smart computed caching with circular dependency detection
- ✅ Error boundaries with retry and fallback
- ✅ Async effects with AbortSignal cancellation
- ✅ Async state with race condition prevention
- ✅ DevTools for debugging reactive systems
- ✅ The complete API reference
Your reactive system is production-ready!