Real-World Examples and API Reference
How to read these examples
Each example below is a complete, runnable scenario. Every global function used comes from the Standalone API. None of them require typing ReactiveUtils. — just plain function calls.
Example 1: Live search with debounced fetch
A search box that fetches results as the user types, cancelling previous requests automatically.
<input id="searchInput" type="text" placeholder="Search...">
<div id="results">Start typing to search</div>
<div id="loader" style="display: none;">Searching...</div>const search = state({ query: '' });
// Track user input
Elements.searchInput.addEventListener('input', (e) => {
search.query = e.target.value;
});
// Async effect cancels the previous fetch when query changes
asyncEffect(async (signal) => {
const q = search.query.trim();
if (!q) {
Elements.results.update({ textContent: 'Start typing to search' });
return;
}
Elements.loader.update({ hidden: false });
const response = await fetch('/api/search?q=' + encodeURIComponent(q), { signal });
const data = await response.json();
Elements.update({
loader: { hidden: true },
results: { innerHTML: data.map(r => `<div class="result">${r.title}</div>`).join('') || 'No results found' }
});
}, {
onError: (error) => {
if (error.name !== 'AbortError') {
Elements.update({
results: { textContent: 'Search failed. Try again.' },
loader: { hidden: true }
});
}
}
});What's happening:
state()tracks the search queryasyncEffect()watchessearch.queryand fetches results- When the user types fast, each new
asyncEffectrun cancels the previous fetch - Only the last result ever renders — no stale data
Example 2: Theme switcher with persistence
A light/dark theme toggle that remembers the user's choice across page loads.
<button id="themeToggle">Toggle Theme</button>
<p id="themeStatus">Current theme: light</p>const settings = state({ theme: 'light' });
// Persist to localStorage — survives page refresh
autoSave(settings, 'app-settings', { debounce: 100 });
// Apply theme to DOM
effect(() => {
document.body.className = 'theme-' + settings.theme;
Elements.themeStatus.update({ textContent: 'Current theme: ' + settings.theme });
});
// Toggle on click
Elements.themeToggle.addEventListener('click', () => {
settings.theme = settings.theme === 'light' ? 'dark' : 'light';
});
// React to changes in OTHER tabs
watchStorage('app-settings', () => {
settings.load(); // reload from localStorage
});What's happening:
state()holds the theme preferenceautoSave()adds localStorage persistence — one line!effect()applies the class whenever theme changeswatchStorage()listens for changes from other tabs and reloads
Example 3: User profile form
A reactive form with validation that shows errors as the user types.
<form id="profileForm">
<input id="nameInput" placeholder="Full name">
<span id="nameError" class="error"></span>
<input id="emailInput" placeholder="Email">
<span id="emailError" class="error"></span>
<button id="submitBtn" disabled>Save Profile</button>
</form>const profileForm = form(
{ name: '', email: '' },
{
validators: {
name: validators.required('Name is required'),
email: validators.combine(
validators.required('Email is required'),
validators.email('Please enter a valid email address')
)
},
onSubmit: async (values) => {
const response = await fetch('/api/profile', {
method: 'PUT',
body: JSON.stringify(values)
});
return response.json();
}
}
);
// Bind inputs
Elements.nameInput.addEventListener('input', (e) => {
profileForm.handleChange('name', e.target.value);
});
Elements.emailInput.addEventListener('input', (e) => {
profileForm.handleChange('email', e.target.value);
});
// Show errors and control submit button reactively
effect(() => {
Elements.nameError.update({ textContent: profileForm.errors.name || '' });
Elements.emailError.update({ textContent: profileForm.errors.email || '' });
Elements.submitBtn.update({ disabled: !profileForm.isValid });
});
// Submit
Elements.profileForm.addEventListener('submit', async (e) => {
e.preventDefault();
const result = await profileForm.submit();
if (result.success) {
alert('Profile saved!');
}
});Example 4: Shopping cart with computed totals
A cart that tracks items, computes totals, and persists across page loads.
<div id="cartItems"></div>
<div id="cartSummary">
<span id="itemCount">0 items</span>
<span id="subtotal">$0.00</span>
<span id="tax">Tax: $0.00</span>
<span id="total">Total: $0.00</span>
</div>
<button id="clearCart">Clear Cart</button>const cart = state({
items: [],
taxRate: 0.08
});
computed(cart, {
subtotal: function() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
tax: function() {
return this.subtotal * this.taxRate;
},
total: function() {
return this.subtotal + this.tax;
},
itemCount: function() {
return this.items.reduce((sum, item) => sum + item.qty, 0);
}
});
autoSave(cart, 'shopping-cart', { debounce: 200 });
// Render cart
effect(() => {
Elements.cartItems.update({
innerHTML: cart.items.map(item => `
<div>
<strong>${item.name}</strong>
$${item.price.toFixed(2)} × ${item.qty}
</div>
`).join('')
});
Elements.update({
itemCount: { textContent: cart.itemCount + ' items' },
subtotal: { textContent: '$' + cart.subtotal.toFixed(2) },
tax: { textContent: 'Tax: $' + cart.tax.toFixed(2) },
total: { textContent: 'Total: $' + cart.total.toFixed(2) }
});
});
// Actions
function addToCart(item) {
batch(() => {
const existing = cart.items.find(i => i.id === item.id);
if (existing) {
existing.qty++;
} else {
cart.items = [...cart.items, { ...item, qty: 1 }];
}
});
}
Elements.clearCart.addEventListener('click', () => {
cart.items = [];
cart.clear(); // also remove from localStorage
});Example 5: Dashboard with protected effects
A dashboard that loads multiple data sources and uses error boundaries to stay resilient.
const dashboard = state({
user: null,
stats: null,
notifications: []
});
const boundary = new ErrorBoundary({
onError: (error) => {
console.error('Dashboard error:', error.message);
},
fallback: () => {
Elements.dashboard.update({ innerHTML: '<p class="error">Dashboard unavailable. Please refresh.</p>' });
}
});
boundary.run(() => {
effect(() => {
if (dashboard.user) {
Elements.welcomeMsg.update({ textContent: 'Welcome back, ' + dashboard.user.name });
}
});
effect(() => {
if (dashboard.stats) {
Elements.update({
totalUsers: { textContent: dashboard.stats.users },
totalRevenue: { textContent: '$' + dashboard.stats.revenue.toFixed(2) }
});
}
});
effect(() => {
const count = dashboard.notifications.length;
Elements.notifBadge.update({ textContent: count, hidden: count === 0 });
});
});
// Load data (using standalone global functions throughout)
const userState = asyncState(null);
const statsState = asyncState(null);
userState.execute(async (signal) => {
const r = await fetch('/api/me', { signal });
dashboard.user = await r.json();
});
statsState.execute(async (signal) => {
const r = await fetch('/api/stats', { signal });
dashboard.stats = await r.json();
});Example 6: Component with full cleanup
A UI component that registers effects and cleans them up when removed from the page.
function createUserCard(userId) {
const userData = state({ name: '', status: 'loading', avatar: null });
// scope() collects all effects and returns one cleanup function
const cleanup = scope((collect) => {
collect(effect(() => {
Id('userName-' + userId).update({ textContent: userData.name });
}));
collect(effect(() => {
Id('userStatus-' + userId).update({
textContent: userData.status,
className: 'status ' + userData.status
});
}));
collect(watch(userData, {
status: (newStatus) => {
console.log('User', userId, 'status changed to', newStatus);
}
}));
});
// Load user data
fetch('/api/users/' + userId)
.then(r => r.json())
.then(data => {
batch(() => {
userData.name = data.name;
userData.status = data.isOnline ? 'online' : 'offline';
userData.avatar = data.avatar;
});
});
// Return cleanup function for the caller
return {
data: userData,
destroy: cleanup
};
}
// Usage:
const card = createUserCard(42);
// Later, when the card is removed from the page:
card.destroy(); // all effects and watchers stopExample 7: Multi-tab notification sync
Using watchStorage() to keep a notification badge in sync across browser tabs.
const notifications = state({ count: 0, items: [] });
// Load initial count from storage
notifications.count = parseInt(localStorage.getItem('notif-count') || '0');
// Show badge
effect(() => {
Elements.notifBadge.update({
textContent: notifications.count,
hidden: notifications.count === 0
});
});
// Watch for changes from other tabs
watchStorage('notif-count', (newCount) => {
notifications.count = parseInt(newCount || '0');
}, { immediate: false });
// When this tab marks notifications as read:
function markAllRead() {
batch(() => {
notifications.count = 0;
notifications.items = [];
});
localStorage.setItem('notif-count', '0');
// Other tabs will pick this up via watchStorage
}Complete API Reference
Core State
| Function | Signature | Returns | Module |
|---|---|---|---|
state() | state(initialValues) | Reactive state object | Core |
createState() | createState(values, bindings) | Reactive state with DOM binding | Core |
effect() | effect(fn) | dispose function | Core |
batch() | batch(fn) | void | Core |
computed() | computed(state, computedDefs) | void | Core |
watch() | watch(state, handlers) | dispose function | Core |
effects() | effects(namedFns) | void | Core |
Refs
| Function | Signature | Returns | Module |
|---|---|---|---|
ref() | ref(initialValue) | { value } reactive ref | Core |
refs() | refs(valuesObj) | object of refs | Core |
Collections
| Function | Signature | Returns | Module |
|---|---|---|---|
collection() | collection(initialArray) | Reactive collection | Core |
list() | list(initialArray) | Reactive collection (alias) | Core |
createCollection() | createCollection(array) | Reactive collection | Collections |
computedCollection() | computedCollection(array, computed) | Collection with computed | Collections |
filteredCollection() | filteredCollection(source, filterFn) | Filtered live view | Collections |
patchArray() | patchArray(state, propName) | void | Core |
Forms
| Function | Signature | Returns | Module |
|---|---|---|---|
form() | form(values, options) | Form state object | Form |
createForm() | createForm(values, options) | Form state object (alias) | Form |
validators | Object of validator factories | — | Form |
Store & Component
| Function | Signature | Returns | Module |
|---|---|---|---|
store() | store(state, options) | Store with getters/actions | Core |
component() | component(options) | Reactive component | Core |
reactive() | reactive(state) | Builder chain | Core |
Bindings
| Function | Signature | Returns | Module |
|---|---|---|---|
bindings() | bindings(bindMap) | void | Core |
updateAll() | updateAll(state, updates) | void | Core |
Enhanced Effects (Module 06)
| Function | Signature | Returns | Module |
|---|---|---|---|
safeEffect() | safeEffect(fn, { errorBoundary }) | dispose | Enhancements |
safeWatch() | safeWatch(state, key, fn, opts) | dispose | Enhancements |
asyncEffect() | asyncEffect(asyncFn, opts) | dispose | Enhancements |
asyncState() | asyncState(initial, opts) | Async state object | Enhancements |
ErrorBoundary | new ErrorBoundary(opts) | ErrorBoundary instance | Enhancements |
DevTools | DevTools.enable() / .trackState() | — | Enhancements |
Async State (Module 06)
| Function | Signature | Returns | Module |
|---|---|---|---|
async() | async(initialValue) | Basic async state | Enhancements |
asyncState() | asyncState(initial, opts) | Enhanced async state | Enhancements |
Cleanup (Module 05)
| Function | Signature | Returns | Module |
|---|---|---|---|
collector() | collector() | { add, cleanup } | Cleanup |
scope() | scope(setupFn) | cleanup function | Cleanup |
Utilities
| Function | Signature | Returns | Module |
|---|---|---|---|
isReactive() | isReactive(value) | boolean | Core |
toRaw() | toRaw(state) | plain object | Core |
notify() | notify(state, key) | void | Core |
pause() | pause() | void | Core |
resume() | resume(flush?) | void | Core |
untrack() | untrack(fn) | fn return value | Core |
Storage (Module 07)
| Function | Signature | Returns | Module |
|---|---|---|---|
autoSave() | autoSave(state, key, opts) | void | Storage |
withStorage() | withStorage(state, key, opts) | void (alias) | Storage |
reactiveStorage() | reactiveStorage(type, namespace) | Reactive storage proxy | Storage |
watchStorage() | watchStorage(key, fn, opts) | void | Storage |
isStorageAvailable() | isStorageAvailable(type) | boolean | Storage |
hasLocalStorage | boolean | — | Storage |
hasSessionStorage | boolean | — | Storage |
Namespace-level (Module 08)
These are also exposed as globals (with conflict-avoidance checks):
| Function | What it does |
|---|---|
set(state, updates) | Functional state update |
cleanup(state) | Clean up all effects/watchers |
getRaw(state) | Get raw object |
execute(asyncState, fn) | Execute async operation |
abort(asyncState) | Abort current operation |
reset(asyncState) | Reset to initial value |
refetch(asyncState) | Re-run last async function |
destroy(component) | Destroy component |
save(state) | Force save to storage |
load(state) | Force load from storage |
clear(state) | Clear from storage |
exists(state) | Check if in storage |
stopAutoSave(state) | Pause auto-saving |
startAutoSave(state) | Resume auto-saving |
storageInfo(state) | Get storage info |
Common mistakes
❌ Loading the shortcut file too early
<!-- WRONG: shortcut before other modules -->
<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>
<!-- ✅ CORRECT: shortcut 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>❌ Using advanced functions without loading their module
// If Module 05 wasn't loaded, collector is undefined
const cleanup = collector(); // ❌ ReferenceError
// ✅ Fix: add the module to your HTML
// <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>❌ Naming conflicts with your own globals
// If you already have a global `state` variable:
var state = 'myString'; // ❌ Overwritten by the shortcut!
// ✅ Fix: use namespace style instead
const myState = ReactiveUtils.state({ count: 0 });❌ Expecting patchArray to be automatic
const s = state({ items: [] });
s.items.push('a'); // ❌ May not trigger effects
patchArray(s, 'items'); // ✅ Patch first
s.items.push('a'); // ✅ Now triggers effectsKey takeaways
- One loader call —
await load('reactive')and gain access to all reactive functions as globals - Seven real-world patterns — search, persistence, forms, carts, dashboards, cleanup, and multi-tab sync
- Full API reference — every function, its signature, return value, and source module
- Always-available vs conditional — core functions always work; advanced/storage/collection functions require their modules
- Load order is the only requirement — get that right and everything else just works
Congratulations!
You've completed the Standalone API learning path. You now understand:
- ✅ What the Standalone API is and why it exists
- ✅ How global functions work and the "alias" concept
- ✅ All core functions in depth (
state,effect,batch,computed,watch, etc.) - ✅ Advanced functions (
safeEffect,asyncEffect,asyncState,ErrorBoundary,DevTools) - ✅ Storage and collection functions (
autoSave,reactiveStorage,watchStorage) - ✅ Real-world examples across 7 practical scenarios
- ✅ The complete API reference
Next steps:
- Explore the Reactive Cleanup module for detailed cleanup patterns
- Explore the Reactive Enhancements module for async state in depth
- Explore the Reactive Storage module for advanced persistence patterns
- Explore the Reactive Form module for complete form management
Happy coding! 🚀