Reactive
What Is Reactive
The reactive module is a self-contained system for managing application state. It lets you create JavaScript objects whose property changes are automatically tracked. When a tracked property changes, any code that depends on it — effects, computed values, DOM bindings — updates automatically without any manual calls.
Simply put: instead of writing code that says "update the UI every time I change this value," you write code that says "this UI element displays this value" — and the reactive system keeps them in sync.
The Recommended Way to Use Reactive
The reactive module exposes all its functions on the ReactiveUtils namespace. However, the recommended approach throughout DOM Helpers is to use the global shortcut functions — the same functions available without any prefix.
Instead of:
// Namespace form — works, but verbose
const app = state({ count: 0 });
effect(() => console.log(app.count));
batch(() => { app.count = 1; });Use the shortcut form:
// Shortcut form — recommended
const app = state({ count: 0 });
effect(() => console.log(app.count));
batch(() => { app.count = 1; });Both are identical in behavior. The shortcut form is cleaner and is what all documentation examples use. Use it consistently.
The Core Functions
state() — Create Reactive State
state() wraps a plain JavaScript object and makes it reactive. Any property you read inside an effect() is automatically tracked. When that property changes, the effect re-runs.
// Create reactive state
const user = state({
name: 'Alice',
email: 'alice@example.com',
isLoggedIn: false,
role: 'viewer'
});
// Change properties normally — no special setter needed
user.name = 'Bob';
user.isLoggedIn = true;The object you get back looks and works like a normal JavaScript object. You read and write properties the same way. The reactivity happens behind the scenes.
effect() — React to State Changes
effect() takes a function and runs it immediately. It also tracks every reactive property the function reads. When any of those properties change, the function runs again automatically.
const counter = state({ value: 0 });
// This runs immediately, then again every time counter.value changes
effect(() => {
Elements.display.textContent = counter.value;
Elements.incrementBtn.disabled = counter.value >= 10;
});
// Somewhere else in your code:
counter.value++; // → the effect runs automatically, DOM updates
counter.value++; // → runs againEffects are the primary way to connect reactive state to DOM updates.
// Multiple state values in one effect — runs when any of them change
const app = state({ count: 0, user: null, loading: false });
effect(() => {
if (app.loading) {
Elements.panel.update({ textContent: 'Loading...', className: 'panel loading' });
return;
}
if (!app.user) {
Elements.panel.update({ textContent: 'Not signed in', className: 'panel empty' });
return;
}
Elements.panel.update({
textContent: 'Welcome, ' + app.user.name,
className: 'panel ready'
});
});computed() — Derived Values
computed() creates a value that is derived from reactive state and updates automatically when its dependencies change.
const cart = state({ items: [], taxRate: 0.08 });
const totals = computed(cart, {
subtotal: function() {
return this.items.reduce((sum, item) => sum + item.price, 0);
},
tax: function() {
return this.subtotal * this.taxRate;
},
total: function() {
return this.subtotal + this.tax;
}
});
// totals.subtotal, totals.tax, totals.total update automatically
// when cart.items or cart.taxRate changes
effect(() => {
Elements.subtotalDisplay.update({ textContent: '$' + totals.subtotal.toFixed(2) });
Elements.taxDisplay.update({ textContent: '$' + totals.tax.toFixed(2) });
Elements.totalDisplay.update({ textContent: '$' + totals.total.toFixed(2) });
});watch() — Observe a Specific Property
watch() calls a callback when a specific property on a reactive object changes. Unlike effect(), it does not run immediately — it only calls the callback when the value actually changes.
const settings = state({ theme: 'light', language: 'en', fontSize: 16 });
// Watch a single property — callback receives new and old value
watch(settings, 'theme', (newTheme, oldTheme) => {
Elements.body.update({ className: 'theme-' + newTheme });
console.log('Theme changed from', oldTheme, 'to', newTheme);
});
watch(settings, 'fontSize', (newSize) => {
Elements.body.update({ style: { fontSize: newSize + 'px' } });
});
// Trigger the watchers
settings.theme = 'dark'; // → watch callback runs
settings.fontSize = 18; // → watch callback runsUse watch() when you want to respond to a specific property change — especially for side effects like logging — without the full re-evaluation that effect() performs.
Storage tip: If you need to persist state to
localStorage, use the reactive module's built-inautoSaveinstead ofwatch+StorageUtils.save. See the storage section below.
batch() — Group Multiple Changes
batch() groups several state changes so that effects and watchers run only once after all the changes are applied, instead of once per individual change.
const form = state({ name: '', email: '', phone: '', role: 'viewer' });
// Without batch — effects run 4 times
form.name = '';
form.email = '';
form.phone = '';
form.role = 'viewer';
// With batch — effects run once, after all four changes
batch(() => {
form.name = '';
form.email = '';
form.phone = '';
form.role = 'viewer';
});Use batch() whenever you reset multiple properties at once, or when a single logical operation modifies several state values.
function resetDashboard() {
batch(() => {
app.user = null;
app.count = 0;
app.notifications = [];
app.loading = false;
app.error = null;
});
}ref() — A Single Reactive Value
ref() creates a reactive container for a single primitive value. Access and set it through its .value property.
// Create a reactive single value
const count = ref(0);
const theme = ref('light');
const isOpen = ref(false);
// Read it
console.log(count.value); // 0
console.log(theme.value); // 'light'
// Change it — triggers any effects that read .value
count.value = 5;
theme.value = 'dark';
isOpen.value = !isOpen.value;
// Use in an effect
effect(() => {
Elements.countDisplay.textContent = count.value;
});Use ref() when your state is a single value rather than an object with multiple properties.
refs() — Multiple Reactive Values at Once
refs() creates several reactive single-value containers from one object definition.
// Create multiple refs at once
const { count, theme, isOpen, userName } = refs({
count: 0,
theme: 'light',
isOpen: false,
userName: ''
});
// Each is an independent ref with its own .value
count.value = 1;
theme.value = 'dark';
isOpen.value = true;
userName.value = 'Alice';
effect(() => {
Elements.greeting.textContent = 'Hello, ' + userName.value;
});Using Reactive with DOM Helpers Core
Reactive state and DOM Helpers core work naturally together. Effects read reactive state and use the core to apply DOM changes:
const app = state({
user: null,
notifications: [],
loading: false
});
// Effect connects state to DOM via the core
effect(() => {
if (app.loading) {
Elements.update({
mainContent: { style: { opacity: '0.5' } },
loadingSpinner: { style: { display: 'block' } }
});
return;
}
Elements.update({
mainContent: { style: { opacity: '1' } },
loadingSpinner: { style: { display: 'none' } },
notificationCount: { textContent: app.notifications.length }
});
if (app.user) {
Elements.update({
userName: { textContent: app.user.name },
userAvatar: { setAttribute: { src: app.user.avatar } },
loginArea: { style: { display: 'none' } },
userArea: { style: { display: 'block' } }
});
}
});
// State changes anywhere trigger the effect
async function loadUser() {
app.loading = true;
const data = await fetch('/api/user').then(r => r.json());
batch(() => {
app.user = data.user;
app.notifications = data.notifications;
app.loading = false;
});
}Using Reactive with Conditions
Reactive state and Conditions work together particularly well. Reactive state provides the value that Conditions watches, and Conditions maps that value to DOM configurations:
// Reactive state holds the current status
const ui = state({ status: 'idle' });
// Conditions declares what each status looks like in the DOM
Conditions.whenState(
() => ui.status,
{
'idle': {
'#submitBtn': { textContent: 'Submit', disabled: false },
'#spinner': { style: { display: 'none' } },
'#successMsg': { style: { display: 'none' } },
'#errorMsg': { style: { display: 'none' } }
},
'loading': {
'#submitBtn': { textContent: 'Submitting...', disabled: true },
'#spinner': { style: { display: 'block' } },
'#successMsg': { style: { display: 'none' } },
'#errorMsg': { style: { display: 'none' } }
},
'success': {
'#submitBtn': { textContent: 'Submit Again', disabled: false },
'#spinner': { style: { display: 'none' } },
'#successMsg': { style: { display: 'block' } },
'#errorMsg': { style: { display: 'none' } }
},
'error': {
'#submitBtn': { textContent: 'Try Again', disabled: false },
'#spinner': { style: { display: 'none' } },
'#successMsg': { style: { display: 'none' } },
'#errorMsg': { style: { display: 'block' } }
}
}
);
// Event handler only changes state — Conditions handles the DOM
Id('myForm').update({
addEventListener: {
submit: async (e) => {
e.preventDefault();
ui.status = 'loading';
try {
await submitForm();
ui.status = 'success';
} catch {
ui.status = 'error';
}
}
}
});Using Reactive with Storage
The reactive module has built-in storage support via autoSave. It automatically loads saved state on startup and persists every change — no manual watch + save calls needed, no StorageUtils required.
// Create reactive state
const prefs = state({
theme: 'light',
fontSize: 16,
language: 'en'
});
// autoSave: loads from localStorage on startup, saves on every change
autoSave(prefs, 'user-prefs');
// Apply state to DOM — already loaded from storage when this runs
effect(() => {
Elements.body.update({
className: 'theme-' + prefs.theme,
style: { fontSize: prefs.fontSize + 'px' }
});
});
// User changes a preference — DOM updates and storage saves automatically
Id('themeToggle').update({
addEventListener: {
click: () => {
prefs.theme = prefs.theme === 'light' ? 'dark' : 'light';
// effect re-runs → DOM updates
// autoSave detects change → localStorage saves automatically
}
}
});autoSave accepts options for debouncing, expiration, cross-tab sync, and more:
autoSave(prefs, 'user-prefs', {
debounce: 300, // wait 300ms after last change before saving
sync: true // sync changes across browser tabs
});Never use
StorageUtilswhen working with reactive state. The reactive module'sautoSaveis the correct tool — it integrates directly with the reactive system and handles load/save/sync automatically.
Available Shortcut Functions
All of the following are available globally — no prefix, no import:
| Function | What it does |
|---|---|
state(object) | Create a reactive object |
effect(fn) | Run a function now and re-run it when its reactive dependencies change |
computed(state, { key: fn }) | Derive values from state that update automatically |
watch(state, 'key', fn) | Call a function when a specific property changes |
batch(fn) | Group multiple state changes into one update cycle |
ref(value) | Create a reactive container for a single value |
refs({ key: value }) | Create multiple reactive single-value containers at once |
autoSave(state, key, opts?) | Persist reactive state to localStorage/sessionStorage automatically |
All of these are equivalent to the same function on ReactiveUtils. The shortcut form is the recommended way to use them throughout your code.
Key Rules for Using Reactive
Rule 1 — Always use the shortcut form. Write state(), effect(), computed() — not ReactiveUtils.state(), ReactiveUtils.effect(). The shortcut form is the standard across all DOM Helpers documentation and examples.
Rule 2 — Read reactive properties inside effects. An effect only tracks the properties it reads during its execution. If you read app.count inside an effect, that effect will re-run when app.count changes. If you do not read it inside the effect, it will not trigger.
Rule 3 — Use batch() for multiple simultaneous changes. If you change five properties in a row, you get five effect runs. Wrap them in batch() to get one run. Use batch for any reset, initialization, or multi-property update.
Rule 4 — Use autoSave for storage, watch() for other side effects. When you need to persist reactive state to localStorage or sessionStorage, use autoSave — not watch + manual save calls. Use watch() for other side effects (logging, API calls) when you only care about a specific property changing. Use effect() when you want to update the DOM based on several properties.
Rule 5 — Connect reactive state to the DOM through effects or Conditions. Reactive state does not touch the DOM directly. Effects read state and use the core (Elements.update(), Id().update()) to apply changes. Or use Conditions to declare the mapping once and have it apply automatically.
What's Next
The reactive module has deep documentation covering every feature:
- Reactive State —
state(), effects, computed, watch in depth - Reactive Utils Shortcut — the full shortcut API and all available global functions
- Reactive Guide — a complete walkthrough from basics to advanced patterns
- Reactive with DOM Helpers Core — combining reactive state with element access and updates
- Reactive with Conditions — state-driven rendering at its most structured
- Reactive Storage —
autoSave,reactiveStorage, cross-tab sync, expiration, and the full storage API
Or continue with the learning path:
- What to Learn Next — the full guided path through all documentation