Skip to content

Sponsor

Sponsor

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.

html
<input id="searchInput" type="text" placeholder="Search...">
<div id="results">Start typing to search</div>
<div id="loader" style="display: none;">Searching...</div>
javascript
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:

  1. state() tracks the search query
  2. asyncEffect() watches search.query and fetches results
  3. When the user types fast, each new asyncEffect run cancels the previous fetch
  4. 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.

html
<button id="themeToggle">Toggle Theme</button>
<p id="themeStatus">Current theme: light</p>
javascript
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:

  1. state() holds the theme preference
  2. autoSave() adds localStorage persistence — one line!
  3. effect() applies the class whenever theme changes
  4. watchStorage() 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.

html
<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>
javascript
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.

html
<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>
javascript
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.

javascript
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.

javascript
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 stop

Example 7: Multi-tab notification sync

Using watchStorage() to keep a notification badge in sync across browser tabs.

javascript
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

FunctionSignatureReturnsModule
state()state(initialValues)Reactive state objectCore
createState()createState(values, bindings)Reactive state with DOM bindingCore
effect()effect(fn)dispose functionCore
batch()batch(fn)voidCore
computed()computed(state, computedDefs)voidCore
watch()watch(state, handlers)dispose functionCore
effects()effects(namedFns)voidCore

Refs

FunctionSignatureReturnsModule
ref()ref(initialValue){ value } reactive refCore
refs()refs(valuesObj)object of refsCore

Collections

FunctionSignatureReturnsModule
collection()collection(initialArray)Reactive collectionCore
list()list(initialArray)Reactive collection (alias)Core
createCollection()createCollection(array)Reactive collectionCollections
computedCollection()computedCollection(array, computed)Collection with computedCollections
filteredCollection()filteredCollection(source, filterFn)Filtered live viewCollections
patchArray()patchArray(state, propName)voidCore

Forms

FunctionSignatureReturnsModule
form()form(values, options)Form state objectForm
createForm()createForm(values, options)Form state object (alias)Form
validatorsObject of validator factoriesForm

Store & Component

FunctionSignatureReturnsModule
store()store(state, options)Store with getters/actionsCore
component()component(options)Reactive componentCore
reactive()reactive(state)Builder chainCore

Bindings

FunctionSignatureReturnsModule
bindings()bindings(bindMap)voidCore
updateAll()updateAll(state, updates)voidCore

Enhanced Effects (Module 06)

FunctionSignatureReturnsModule
safeEffect()safeEffect(fn, { errorBoundary })disposeEnhancements
safeWatch()safeWatch(state, key, fn, opts)disposeEnhancements
asyncEffect()asyncEffect(asyncFn, opts)disposeEnhancements
asyncState()asyncState(initial, opts)Async state objectEnhancements
ErrorBoundarynew ErrorBoundary(opts)ErrorBoundary instanceEnhancements
DevToolsDevTools.enable() / .trackState()Enhancements

Async State (Module 06)

FunctionSignatureReturnsModule
async()async(initialValue)Basic async stateEnhancements
asyncState()asyncState(initial, opts)Enhanced async stateEnhancements

Cleanup (Module 05)

FunctionSignatureReturnsModule
collector()collector(){ add, cleanup }Cleanup
scope()scope(setupFn)cleanup functionCleanup

Utilities

FunctionSignatureReturnsModule
isReactive()isReactive(value)booleanCore
toRaw()toRaw(state)plain objectCore
notify()notify(state, key)voidCore
pause()pause()voidCore
resume()resume(flush?)voidCore
untrack()untrack(fn)fn return valueCore

Storage (Module 07)

FunctionSignatureReturnsModule
autoSave()autoSave(state, key, opts)voidStorage
withStorage()withStorage(state, key, opts)void (alias)Storage
reactiveStorage()reactiveStorage(type, namespace)Reactive storage proxyStorage
watchStorage()watchStorage(key, fn, opts)voidStorage
isStorageAvailable()isStorageAvailable(type)booleanStorage
hasLocalStoragebooleanStorage
hasSessionStoragebooleanStorage

Namespace-level (Module 08)

These are also exposed as globals (with conflict-avoidance checks):

FunctionWhat 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

html
<!-- 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

javascript
// 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

javascript
// 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

javascript
const s = state({ items: [] });

s.items.push('a');         // ❌ May not trigger effects

patchArray(s, 'items');    // ✅ Patch first
s.items.push('a');         // ✅ Now triggers effects

Key takeaways

  1. One loader callawait load('reactive') and gain access to all reactive functions as globals
  2. Seven real-world patterns — search, persistence, forms, carts, dashboards, cleanup, and multi-tab sync
  3. Full API reference — every function, its signature, return value, and source module
  4. Always-available vs conditional — core functions always work; advanced/storage/collection functions require their modules
  5. 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:

Happy coding! 🚀