Understanding bindings() - A Beginner's Guide
Quick Start (30 seconds)
Need to automatically sync state with DOM elements? Here's how:
// Create reactive state
const user = state({
name: 'John',
email: 'john@example.com',
isOnline: true
});
// Bind state to DOM elements using CSS selectors
bindings({
'#userName': () => user.name,
'#userEmail': () => user.email,
'#status': () => user.isOnline ? 'Online' : 'Offline'
});
// Changes automatically update the DOM!
user.name = 'Jane';
// #userName element now displays "Jane"
user.isOnline = false;
// #status element now displays "Offline"That's it! The bindings() function creates automatic connections between reactive state and DOM elements using CSS selectors.
What is bindings()?
bindings() is a function that creates automatic two-way connections between reactive state and DOM elements. It uses CSS selectors to find elements and keeps them synchronized with your reactive data.
The bindings() function:
- Uses CSS selectors to target DOM elements
- Automatically updates elements when state changes
- Supports text content, properties, attributes, and styles
- Works with multiple elements via class selectors
- Returns a cleanup function to stop all bindings
Think of it as wiring your state directly to your HTML - you define which elements should display which data, and they automatically stay synchronized.
Syntax
// Using the full namespace
bindings(definitions)
// Or using ReactiveUtils namespace
ReactiveUtils.bindings(definitions)Parameters:
definitions- An object where:- Keys are CSS selectors (e.g.,
'#id','.class','div') - Values are either:
- Functions that return values to display
- Strings (property names from state)
- Objects for multiple property bindings
- Keys are CSS selectors (e.g.,
Returns:
- A cleanup function that stops all bindings when called
Example:
const cleanup = bindings({
'#display': () => counter.count,
'.status': () => app.isActive ? 'Active' : 'Inactive'
});
// Later, stop all bindings
cleanup();Why Does This Exist?
The Challenge with Plain JavaScript
In vanilla JavaScript, keeping DOM elements synchronized with data requires manual updates:
// Plain JavaScript approach
let count = 0;
let userName = 'John';
let isOnline = false;
function updateUI() {
// Manually find and update each element
document.getElementById('count').textContent = count;
document.getElementById('userName').textContent = userName;
const statusElement = document.getElementById('status');
statusElement.textContent = isOnline ? 'Online' : 'Offline';
statusElement.className = isOnline ? 'online' : 'offline';
// Update all elements with class 'user-name'
document.querySelectorAll('.user-name').forEach(el => {
el.textContent = userName;
});
}
// You must call updateUI after every change
count = 5;
updateUI(); // ❌ Easy to forget
userName = 'Jane';
updateUI(); // ❌ Repetitive
isOnline = true;
updateUI(); // ❌ TediousProblems with this approach: ❌ Manual DOM queries for every update ❌ Must remember to call update function after each change ❌ Repetitive querySelector/getElementById calls ❌ Must update all affected elements manually ❌ Easy to forget elements, causing UI desynchronization ❌ Code becomes scattered and hard to maintain
What Situation Is This Designed For?
Applications need to display reactive data in the DOM:
- Showing user information in multiple places
- Displaying live counters and statistics
- Updating status indicators
- Reflecting form data in preview areas
- Synchronizing settings across UI elements
- Any scenario where DOM elements need to stay in sync with data
Manually coordinating all these DOM updates is tedious and error-prone. bindings() is designed specifically to automate this synchronization.
How Does bindings() Help?
With bindings(), you declare once which elements should display which data:
const app = state({
count: 0,
userName: 'John',
isOnline: false
});
// Define bindings with CSS selectors
bindings({
'#count': () => app.count,
'#userName': () => app.userName,
'.user-name': () => app.userName, // All matching elements
'#status': () => app.isOnline ? 'Online' : 'Offline'
});
// Just change the data—DOM updates automatically! ✨
app.count = 5; // #count updates automatically
app.userName = 'Jane'; // #userName and .user-name update
app.isOnline = true; // #status updates automaticallyBenefits: ✅ Automatic DOM synchronization ✅ CSS selectors for clean, familiar syntax ✅ No manual querySelector calls needed ✅ Impossible to forget to update elements ✅ Declarative, easy to understand ✅ Works with multiple elements at once
When Does bindings() Shine?
This method is particularly well-suited when:
- You need to display reactive data in the DOM
- You want automatic UI updates without manual DOM manipulation
- You're working with existing HTML and want to add reactivity
- You need to update multiple elements with the same data
- You want clean, declarative data-to-DOM connections
- You're building progressive enhancement on top of HTML
Mental Model
Think of bindings() like power outlets connecting appliances:
Manual DOM Updates (Unplugged):
┌─────────────┐
│ Data: 5 │ Your data
└─────────────┘
│
▼
[Manual wiring needed]
│
▼
┌─────────────┐
│ <div>5</div>│ ← You must manually update
└─────────────┘
❌ Change data → manually update DOM
❌ Forget once → UI out of sync
Reactive Bindings (Plugged In):
┌─────────────────┐
│ count: 5 │ Your data
└────────┬────────┘
│
▼
┌─────────────────┐
│ '#counter' │ ← Binding (like a power cord)
└────────┬────────┘
│
▼
┌─────────────────┐
│ <div id="counter">5</div>│ DOM element
└─────────────────┘
✅ Change data → DOM updates automatically
✅ Always in sync
✅ No manual work neededKey Insight: Just like plugging an appliance into an outlet provides automatic power, creating a binding provides automatic data flow from state to DOM. You set it up once, and it works continuously.
How Does It Work?
The Magic: Effects + Selectors
When you call bindings(), it creates effects for each selector:
bindings({
'#counter': () => count.value
})
│
▼
┌────────────────────────┐
│ For Each Selector: │
│ 1. Find elements │
│ 2. Create effect │
│ 3. Update on change │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Effect watches state │
│ Updates DOM elements │
└───────────┬────────────┘
│
▼
When state changes:
→ Effect runs
→ Reads new value
→ Updates elementUnder the hood (simplified):
function bindings(defs) {
const cleanups = [];
Object.entries(defs).forEach(([selector, binding]) => {
// Find elements
const elements = document.querySelectorAll(selector);
// Create effect for each binding
const cleanup = effect(() => {
const value = typeof binding === 'function' ? binding() : binding;
elements.forEach(el => {
el.textContent = value; // Simplified
});
});
cleanups.push(cleanup);
});
return () => cleanups.forEach(c => c());
}What happens:
1️⃣ You provide selectors and binding functions 2️⃣ bindings() finds all matching DOM elements 3️⃣ For each binding, it creates an effect 4️⃣ The effect reads reactive state and updates the element 5️⃣ When state changes, effects re-run and update the DOM
This is automatic and continuous until you call the cleanup function!
Basic Usage
Binding to ID Selectors
The most common pattern is binding to element IDs:
const counter = state({
count: 0
});
bindings({
'#display': () => counter.count
});
// <div id="display">0</div> initially
// Updates automatically when counter.count changes
counter.count = 5;
// <div id="display">5</div>Binding to Class Selectors
Bind to all elements with a class:
const user = state({
name: 'John'
});
bindings({
'.user-name': () => user.name
});
// <span class="user-name">John</span>
// <div class="user-name">John</div>
// <p class="user-name">John</p>
// All update simultaneously!
user.name = 'Jane';
// All three elements now show "Jane"Binding to Tag Selectors
Bind to all elements of a type:
const config = state({
appName: 'MyApp'
});
bindings({
'title': () => config.appName
});
// <title>MyApp</title>
config.appName = 'AwesomeApp';
// <title>AwesomeApp</title>Binding to Text Content
Simple Text Display
By default, bindings set textContent:
const message = state({
text: 'Hello World'
});
bindings({
'#message': () => message.text
});
// <div id="message">Hello World</div>
message.text = 'Goodbye';
// <div id="message">Goodbye</div>Computed Text
Use functions to compute display values:
const cart = state({
items: 5,
total: 99.99
});
bindings({
'#itemCount': () => `${cart.items} items`,
'#total': () => `$${cart.total.toFixed(2)}`,
'#summary': () => `${cart.items} items - Total: $${cart.total.toFixed(2)}`
});
cart.items = 3;
cart.total = 75.50;
// All elements update automaticallyConditional Text
const user = state({
isLoggedIn: false,
name: ''
});
bindings({
'#greeting': () => user.isLoggedIn ? `Welcome, ${user.name}!` : 'Please log in'
});
user.isLoggedIn = true;
user.name = 'John';
// <div id="greeting">Welcome, John!</div>Binding to Element Properties
Binding to Specific Properties
Use an object to bind to specific properties:
const form = state({
email: '',
isValid: false
});
bindings({
'#emailInput': {
value: () => form.email,
disabled: () => !form.isValid
}
});
// <input id="emailInput" value="" disabled>
form.email = 'john@example.com';
form.isValid = true;
// <input id="emailInput" value="john@example.com">Binding to className
const app = state({
theme: 'light',
isLoading: false
});
bindings({
'body': {
className: () => `theme-${app.theme} ${app.isLoading ? 'loading' : ''}`
}
});
app.theme = 'dark';
app.isLoading = true;
// <body class="theme-dark loading">Binding to Styles
const slider = state({
value: 50
});
bindings({
'#progress': {
style: () => ({ width: `${slider.value}%` })
}
});
slider.value = 75;
// <div id="progress" style="width: 75%;">Binding with Functions
Dynamic Calculations
const rect = state({
width: 100,
height: 50
});
bindings({
'#area': () => rect.width * rect.height,
'#perimeter': () => 2 * (rect.width + rect.height),
'#aspectRatio': () => (rect.width / rect.height).toFixed(2)
});
rect.width = 200;
// All three elements update with new calculationsConditional Logic
const account = state({
balance: 100
});
bindings({
'#status': () => {
if (account.balance > 1000) return 'Premium';
if (account.balance > 100) return 'Standard';
return 'Basic';
},
'#statusClass': {
className: () => {
if (account.balance > 1000) return 'status-premium';
if (account.balance > 100) return 'status-standard';
return 'status-basic';
}
}
});Using Multiple State Objects
const user = state({ name: 'John' });
const settings = state({ showFullName: true });
bindings({
'#display': () => settings.showFullName ? user.name : user.name.charAt(0)
});
// Depends on both user.name AND settings.showFullNameMultiple Bindings Per Element
Binding Multiple Properties
const input = state({
value: '',
placeholder: 'Enter text',
isDisabled: false,
maxLength: 100
});
bindings({
'#myInput': {
value: () => input.value,
placeholder: () => input.placeholder,
disabled: () => input.isDisabled,
maxLength: () => input.maxLength
}
});
input.placeholder = 'Type something...';
input.maxLength = 50;
// Element updates both propertiesCombining Text and Properties
const button = state({
text: 'Click Me',
isDisabled: false,
count: 0
});
bindings({
'#actionButton': {
textContent: () => `${button.text} (${button.count})`,
disabled: () => button.isDisabled,
className: () => button.isDisabled ? 'btn-disabled' : 'btn-active'
}
});
button.count = 5;
button.text = 'Submit';
// <button id="actionButton" class="btn-active">Submit (5)</button>Selector Types
ID Selectors (#id)
Most specific, fastest:
bindings({
'#header': () => app.title,
'#footer': () => app.copyright
});Class Selectors (.class)
Update all matching elements:
bindings({
'.price': () => `$${product.price}`,
'.discount': () => `${product.discount}%`
});
// Updates all elements with class="price"
// Updates all elements with class="discount"Tag Selectors (tag)
Update all elements of a type:
bindings({
'h1': () => site.title,
'footer': () => site.footerText
});Complex Selectors
Use any valid CSS selector:
bindings({
'div.user > span.name': () => user.name,
'[data-role="admin"]': () => user.isAdmin ? 'visible' : 'hidden',
'nav ul li:first-child': () => menu.firstItem
});Real-World Examples
Example 1: User Profile Card
const user = state({
name: 'John Doe',
email: 'john@example.com',
avatar: 'avatar.jpg',
isOnline: true,
lastSeen: new Date(),
postsCount: 42,
followersCount: 150
});
bindings({
'#userName': () => user.name,
'#userEmail': () => user.email,
'#userAvatar': {
src: () => user.avatar,
alt: () => `${user.name}'s avatar`
},
'#onlineStatus': () => user.isOnline ? '🟢 Online' : '🔴 Offline',
'.status-dot': {
className: () => user.isOnline ? 'online' : 'offline'
},
'#lastSeen': () => user.isOnline
? 'Online now'
: `Last seen: ${user.lastSeen.toLocaleString()}`,
'#postsCount': () => user.postsCount.toLocaleString(),
'#followersCount': () => user.followersCount.toLocaleString()
});
// Update user data
user.isOnline = false;
user.postsCount = 45;
// UI automatically updatesExample 2: Shopping Cart
const cart = state({
items: [
{ id: 1, name: 'Laptop', price: 999, quantity: 1 },
{ id: 2, name: 'Mouse', price: 25, quantity: 2 }
],
discountCode: '',
taxRate: 0.1
});
// Add computed properties
computed(cart, {
subtotal() {
return this.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
},
discount() {
return this.discountCode === 'SAVE10' ? this.subtotal * 0.1 : 0;
},
tax() {
return (this.subtotal - this.discount) * this.taxRate;
},
total() {
return this.subtotal - this.discount + this.tax;
}
});
bindings({
'#itemCount': () => cart.items.reduce((sum, item) => sum + item.quantity, 0),
'#subtotal': () => `$${cart.subtotal.toFixed(2)}`,
'#discount': () => cart.discount > 0
? `-$${cart.discount.toFixed(2)}`
: '$0.00',
'#tax': () => `$${cart.tax.toFixed(2)}`,
'#total': () => `$${cart.total.toFixed(2)}`,
'#emptyMessage': {
style: () => ({ display: cart.items.length === 0 ? 'block' : 'none' })
}
});
// Add item
cart.items.push({ id: 3, name: 'Keyboard', price: 75, quantity: 1 });
// All displays update automaticallyExample 3: Live Dashboard
const dashboard = state({
visitors: 0,
pageViews: 0,
avgSessionTime: 0,
bounceRate: 0,
topPage: '/home',
lastUpdate: new Date()
});
bindings({
'#visitors': () => dashboard.visitors.toLocaleString(),
'#pageViews': () => dashboard.pageViews.toLocaleString(),
'#sessionTime': () => `${dashboard.avgSessionTime}s`,
'#bounceRate': () => `${dashboard.bounceRate.toFixed(1)}%`,
'#bounceRateClass': {
className: () => {
if (dashboard.bounceRate < 40) return 'metric excellent';
if (dashboard.bounceRate < 60) return 'metric good';
return 'metric poor';
}
},
'#topPage': () => dashboard.topPage,
'#lastUpdate': () => dashboard.lastUpdate.toLocaleTimeString(),
'.trend-indicator': () => {
const growth = ((dashboard.pageViews / dashboard.visitors) - 1) * 100;
return growth > 0 ? `📈 +${growth.toFixed(1)}%` : `📉 ${growth.toFixed(1)}%`;
}
});
// Simulate live updates
setInterval(() => {
dashboard.visitors += Math.floor(Math.random() * 10);
dashboard.pageViews += Math.floor(Math.random() * 20);
dashboard.avgSessionTime = Math.floor(Math.random() * 300);
dashboard.bounceRate = 30 + Math.random() * 40;
dashboard.lastUpdate = new Date();
}, 2000);Example 4: Form with Live Preview
const form = state({
title: '',
description: '',
category: 'general',
tags: [],
isPublished: false
});
bindings({
// Live preview
'#previewTitle': () => form.title || '(No title)',
'#previewDescription': () => form.description || '(No description)',
'#previewCategory': () => form.category.toUpperCase(),
'#previewTags': () => form.tags.join(', ') || '(No tags)',
'#previewStatus': () => form.isPublished ? '✅ Published' : '📝 Draft',
// Form validation indicators
'#titleCounter': () => `${form.title.length}/100`,
'#titleWarning': {
style: () => ({
display: form.title.length > 80 ? 'block' : 'none'
})
},
'#descCounter': () => `${form.description.length}/500`,
// Submit button
'#submitButton': {
disabled: () => !form.title || !form.description,
textContent: () => form.isPublished ? 'Update' : 'Publish'
}
});
// User types in form
form.title = 'My Blog Post';
form.description = 'This is an interesting article about reactive programming.';
// Preview updates in real-timeExample 5: Settings Panel
const settings = state({
theme: 'light',
fontSize: 14,
notifications: true,
autoSave: true,
language: 'en'
});
bindings({
// Apply settings
'body': {
className: () => `theme-${settings.theme} font-${settings.fontSize}`
},
// Setting displays
'#themeDisplay': () => settings.theme === 'light' ? '☀️ Light' : '🌙 Dark',
'#fontSizeDisplay': () => `${settings.fontSize}px`,
'#notificationStatus': () => settings.notifications ? 'Enabled' : 'Disabled',
'#autoSaveStatus': () => settings.autoSave ? 'On' : 'Off',
'#languageDisplay': () => settings.language.toUpperCase(),
// Toggle buttons
'#themeToggle': {
textContent: () => settings.theme === 'light'
? 'Switch to Dark'
: 'Switch to Light'
},
'#notificationToggle': {
className: () => settings.notifications ? 'toggle-on' : 'toggle-off'
},
// Preview message
'#settingsPreview': () =>
`Your ${settings.theme} theme with ${settings.fontSize}px font is active.`
});
// Change settings
settings.theme = 'dark';
settings.fontSize = 16;
// Entire UI updates automaticallyCommon Pitfalls
Pitfall #1: Elements Not Found
❌ Wrong:
bindings({
'#myElement': () => state.value
});
// But #myElement doesn't exist in DOM yet
// Binding silently fails✅ Correct:
// Make sure DOM elements exist first
document.addEventListener('DOMContentLoaded', () => {
bindings({
'#myElement': () => state.value
});
});
// Or check if element exists
if (document.getElementById('myElement')) {
bindings({
'#myElement': () => state.value
});
}What's happening:
- Bindings need elements to exist in the DOM
- Create bindings after DOM is ready
- Or use conditional checks
Pitfall #2: Forgetting to Return Values in Functions
❌ Wrong:
bindings({
'#display': () => {
state.count * 2; // Missing return!
}
});
// Element shows "undefined"✅ Correct:
bindings({
'#display': () => {
return state.count * 2; // Return the value
}
});
// Or use arrow function shorthand
bindings({
'#display': () => state.count * 2
});What's happening:
- Binding functions must return values
- Without return, the function returns undefined
- Use arrow function shorthand for simple returns
Pitfall #3: Not Cleaning Up Bindings
❌ Wrong:
function createWidget() {
const state = ReactiveUtils.state({ value: 0 });
bindings({
'.widget': () => state.value
});
return state;
}
// Create many widgets
for (let i = 0; i < 100; i++) {
createWidget();
}
// ❌ Memory leak: all bindings still active!✅ Correct:
function createWidget() {
const state = ReactiveUtils.state({ value: 0 });
const cleanup = bindings({
'.widget': () => state.value
});
return {
state,
destroy: cleanup
};
}
// Create and properly destroy
const widgets = [];
for (let i = 0; i < 100; i++) {
widgets.push(createWidget());
}
// Later, clean up
widgets.forEach(widget => widget.destroy());What's happening:
- Always save and call the cleanup function
- This stops the effects and prevents memory leaks
- Essential for dynamic content
Pitfall #4: Expensive Computations in Bindings
❌ Wrong:
const data = state({
numbers: Array.from({ length: 100000 }, (_, i) => i)
});
bindings({
'#result': () => {
// ❌ Expensive calculation runs on every change
return data.numbers.reduce((sum, n) => sum + n, 0);
}
});✅ Correct:
const data = state({
numbers: Array.from({ length: 100000 }, (_, i) => i)
});
// Use computed for expensive calculations (cached)
computed(data, {
sum() {
return this.numbers.reduce((sum, n) => sum + n, 0);
}
});
bindings({
'#result': () => data.sum // Use cached computed value
});What's happening:
- Bindings create effects that run frequently
- Heavy computations should be in computed properties
- Computed properties are cached and only recalculate when needed
Pitfall #5: Trying to Bind to Non-String Values Incorrectly
❌ Wrong:
const data = state({
items: [1, 2, 3]
});
bindings({
'#list': () => data.items // Shows "[object Object]" or similar
});✅ Correct:
const data = state({
items: [1, 2, 3]
});
bindings({
'#list': () => data.items.join(', ') // Convert to string
});
// Or create proper HTML
bindings({
'#list': {
innerHTML: () => data.items.map(item => `<li>${item}</li>`).join('')
}
});What's happening:
- Text bindings need string values
- Convert arrays/objects to strings
- Or use innerHTML for HTML content
Summary
What is bindings()?
bindings() creates automatic connections between reactive state and DOM elements using CSS selectors, keeping them synchronized without manual updates.
Why use bindings() instead of manual DOM updates?
- Automatic synchronization
- CSS selectors for familiar, clean syntax
- No manual querySelector/getElementById calls
- Declarative data-to-DOM connections
- Updates multiple elements at once
- Impossible to forget to update UI
Key Points to Remember:
1️⃣ Uses CSS selectors - #id, .class, tag, or complex selectors
2️⃣ Automatic updates - DOM stays synchronized with state
3️⃣ Multiple bindings - Can bind text, properties, styles, and attributes
4️⃣ Returns cleanup - Always save and call it when done
5️⃣ Elements must exist - Create bindings after DOM is ready
6️⃣ Functions must return - Binding functions need to return values
7️⃣ Use computed for heavy calculations - Keep bindings fast and simple
Mental Model: Think of bindings() like power outlets - they provide automatic, continuous flow from state to DOM. Plug them in once, and they keep everything synchronized.
Quick Reference:
// Create state
const app = state({
title: 'MyApp',
count: 0,
isActive: true
});
// Create bindings
const cleanup = bindings({
// Simple text
'#title': () => app.title,
// Computed text
'#count': () => `Count: ${app.count}`,
// Multiple properties
'#status': {
textContent: () => app.isActive ? 'Active' : 'Inactive',
className: () => app.isActive ? 'active' : 'inactive'
},
// All elements with class
'.app-name': () => app.title
});
// Changes automatically update DOM
app.title = 'AwesomeApp';
app.count = 5;
// Clean up when done
cleanup();Remember: bindings() is your tool for automatic state-to-DOM synchronization. It eliminates manual DOM updates and keeps your UI perfectly synchronized with your reactive data! ✨