Understanding effects() - A Beginner's Guide
Quick Start (30 seconds)
Need to create multiple related effects at once? Here's how:
// Create reactive state
const app = state({
user: { name: 'John', role: 'admin' },
theme: 'light',
notifications: 0
});
// Create multiple effects with one call
effects({
updateUserDisplay() {
document.getElementById('userName').textContent = app.user.name;
document.getElementById('userRole').textContent = app.user.role;
},
applyTheme() {
document.body.className = `theme-${app.theme}`;
},
updateNotificationBadge() {
const badge = document.getElementById('badge');
badge.textContent = app.notifications;
badge.style.display = app.notifications > 0 ? 'block' : 'none';
}
});
// All effects run immediately and automatically update when data changes!
app.theme = 'dark';
app.notifications = 5;That's it! The effects() function creates multiple named effects in one organized call.
What is effects()?
effects() is a function that creates multiple reactive effects from an object definition. It's a convenient way to organize and set up several related effects at once, each with a descriptive name.
The effects() function:
- Creates multiple effects in one call
- Gives each effect a descriptive name
- Returns a single cleanup function that stops all effects
- Helps organize related reactive behavior
- Makes code more readable and maintainable
Think of it as batch creation of effects - instead of calling effect() multiple times, you define all your effects together in an organized structure.
Syntax
// Using the full namespace
effects(definitions)
// Or using ReactiveUtils namespace
ReactiveUtils.effects(definitions)Parameters:
definitions- An object where each key is a descriptive name and each value is an effect function
Returns:
- A single cleanup function that stops all effects when called
Example:
const cleanup = effects({
logCount() {
console.log(`Count: ${counter.count}`);
},
updateDisplay() {
document.getElementById('display').textContent = counter.count;
}
});
// Later, stop all effects at once
cleanup();Why Does This Exist?
The Challenge with Creating Multiple Effects
When you need several related effects, calling effect() repeatedly can become verbose:
// Creating effects one by one
const cleanup1 = effect(() => {
document.getElementById('userName').textContent = user.name;
});
const cleanup2 = effect(() => {
document.getElementById('userEmail').textContent = user.email;
});
const cleanup3 = effect(() => {
document.getElementById('userRole').textContent = user.role;
});
const cleanup4 = effect(() => {
const statusDot = document.getElementById('status');
statusDot.className = user.isOnline ? 'online' : 'offline';
});
// Managing cleanup becomes tedious
function cleanupAll() {
cleanup1();
cleanup2();
cleanup3();
cleanup4();
}Problems with this approach: ❌ Repetitive effect() calls ❌ Multiple cleanup functions to manage ❌ No descriptive names for debugging ❌ Related effects scattered across code ❌ Tedious to track and clean up
What Situation Is This Designed For?
Applications often need to set up multiple related effects:
- Component initialization with several UI updates
- Dashboard with multiple live displays
- Form with multiple field validations
- Application with various synchronized features
- Any scenario where multiple pieces of UI need to stay synchronized
Creating and managing these individually is tedious. effects() is designed specifically for organizing multiple effects together.
How Does effects() Help?
With effects(), you create all related effects in one organized definition:
const cleanup = effects({
updateUserName() {
document.getElementById('userName').textContent = user.name;
},
updateUserEmail() {
document.getElementById('userEmail').textContent = user.email;
},
updateUserRole() {
document.getElementById('userRole').textContent = user.role;
},
updateOnlineStatus() {
const statusDot = document.getElementById('status');
statusDot.className = user.isOnline ? 'online' : 'offline';
}
});
// Single cleanup function for all effects
// When done: cleanup();Benefits: ✅ All effects in one organized structure ✅ Descriptive names for each effect ✅ Single cleanup function for all ✅ Related effects grouped together ✅ More readable and maintainable ✅ Easier debugging with named effects
When Does effects() Shine?
This method is particularly well-suited when:
- You need multiple related effects
- You want to organize effects with descriptive names
- You're building components with several UI updates
- You need clean, grouped cleanup
- You want more maintainable effect organization
- You're setting up complex reactive behavior
Mental Model
Think of effects() like an event venue setup checklist:
Individual effect() Calls (Separate Tasks):
┌─────────────────────┐
│ Set up lights │ ← Task 1, cleanup1()
└─────────────────────┘
┌─────────────────────┐
│ Set up sound │ ← Task 2, cleanup2()
└─────────────────────┘
┌─────────────────────┐
│ Arrange seating │ ← Task 3, cleanup3()
└─────────────────────┘
┌─────────────────────┐
│ Prepare catering │ ← Task 4, cleanup4()
└─────────────────────┘
❌ Multiple cleanup functions
❌ Hard to track
❌ Easy to forget one
effects() (Organized Checklist):
┌─────────────────────────────┐
│ VENUE SETUP CHECKLIST │
│ ✓ setUpLights() │
│ ✓ setUpSound() │
│ ✓ arrangeSeating() │
│ ✓ prepareCatering() │
└─────────────────────────────┘
│
▼
Single cleanup()
✓ All organized
✓ Easy to track
✓ One function cleans everythingKey Insight: Just like an organized checklist groups related tasks together and makes them easier to manage, effects() groups related effects together with descriptive names and provides unified cleanup.
How Does It Work?
The Magic: Batch Effect Creation
When you call effects(), it creates individual effects for each function in your definition:
effects({
logCount() { ... },
updateUI() { ... }
})
│
▼
┌────────────────────┐
│ For Each Entry: │
│ 1. Create effect │
│ 2. Store cleanup │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Return Unified │
│ Cleanup Function │
└────────────────────┘What happens:
1️⃣ You provide an object with named effect functions 2️⃣ effects() iterates over each entry 3️⃣ For each entry, it creates an effect using effect() 4️⃣ It collects all the individual cleanup functions 5️⃣ Returns a single function that calls all cleanups
Under the hood (simplified):
function effects(definitions) {
const cleanups = [];
Object.values(definitions).forEach(fn => {
const cleanup = effect(fn);
cleanups.push(cleanup);
});
return () => {
cleanups.forEach(cleanup => cleanup());
};
}This is just a convenience - you could create effects individually, but effects() makes it more organized and manageable!
Basic Usage
Creating Multiple Effects
The simplest way to use effects() is to define multiple effect functions:
const counter = state({
count: 0
});
effects({
logToConsole() {
console.log(`Count: ${counter.count}`);
},
updateDisplay() {
document.getElementById('display').textContent = counter.count;
},
updateDoubled() {
document.getElementById('doubled').textContent = counter.count * 2;
}
});
// All three effects run immediately
// All automatically update when counter.count changes
counter.count = 5;
// All three effects re-run automaticallyEach Effect is Independent
Each effect tracks its own dependencies:
const app = state({
user: 'John',
theme: 'light',
count: 0
});
effects({
updateUser() {
console.log(`User: ${app.user}`);
},
updateTheme() {
console.log(`Theme: ${app.theme}`);
},
updateCount() {
console.log(`Count: ${app.count}`);
}
});
app.user = 'Jane';
// Only updateUser re-runs
app.theme = 'dark';
// Only updateTheme re-runs
app.count = 5;
// Only updateCount re-runsOrganizing Multiple Effects
Grouping Related UI Updates
const dashboard = state({
sales: 1000,
visitors: 250,
revenue: 5000,
conversionRate: 4.2
});
effects({
displaySales() {
document.getElementById('sales').textContent = `$${dashboard.sales}`;
},
displayVisitors() {
document.getElementById('visitors').textContent = dashboard.visitors;
},
displayRevenue() {
document.getElementById('revenue').textContent = `$${dashboard.revenue}`;
},
displayConversionRate() {
const element = document.getElementById('conversion');
element.textContent = `${dashboard.conversionRate}%`;
// Color coding
if (dashboard.conversionRate > 5) {
element.className = 'excellent';
} else if (dashboard.conversionRate > 3) {
element.className = 'good';
} else {
element.className = 'poor';
}
}
});Organizing Form Validations
const form = state({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const errors = state({
username: '',
email: '',
password: '',
confirmPassword: ''
});
effects({
validateUsername() {
if (!form.username) {
errors.username = '';
} else if (form.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
} else {
errors.username = '';
}
},
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!form.email) {
errors.email = '';
} else if (!emailRegex.test(form.email)) {
errors.email = 'Please enter a valid email';
} else {
errors.email = '';
}
},
validatePassword() {
if (!form.password) {
errors.password = '';
} else if (form.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
} else {
errors.password = '';
}
},
validateConfirmPassword() {
if (!form.confirmPassword) {
errors.confirmPassword = '';
} else if (form.password !== form.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
} else {
errors.confirmPassword = '';
}
}
});Named Effects for Clarity
Descriptive Names Aid Debugging
Using descriptive names makes debugging easier:
const app = state({
currentPage: '/home',
user: null,
notifications: 0
});
effects({
trackPageView() {
console.log(`Page viewed: ${app.currentPage}`);
sendAnalytics('page_view', { page: app.currentPage });
},
updatePageTitle() {
document.title = `MyApp - ${app.currentPage}`;
},
syncUserSession() {
if (app.user) {
localStorage.setItem('user', JSON.stringify(app.user));
}
},
displayNotificationBadge() {
const badge = document.getElementById('notificationBadge');
badge.textContent = app.notifications;
badge.style.display = app.notifications > 0 ? 'inline' : 'none';
}
});
// In console/debugger, you can see which effect is running
// Error messages will show the effect nameSelf-Documenting Code
Names explain what each effect does:
const player = state({
isPlaying: false,
currentTime: 0,
volume: 0.5,
isMuted: false
});
effects({
togglePlayPauseButton() {
const button = document.getElementById('playPause');
button.textContent = player.isPlaying ? '⏸' : '▶️';
},
updateProgressBar() {
const progress = document.getElementById('progress');
progress.value = player.currentTime;
},
applyVolumeLevel() {
const volumeSlider = document.getElementById('volume');
volumeSlider.value = player.volume;
audioElement.volume = player.isMuted ? 0 : player.volume;
},
toggleMuteIcon() {
const icon = document.getElementById('muteIcon');
icon.textContent = player.isMuted ? '🔇' : '🔊';
}
});
// Names make it clear what each effect does without reading codeCleanup and Disposal
Single Cleanup for All Effects
The main advantage of effects() is unified cleanup:
const app = state({
data: [],
loading: false,
error: null
});
const cleanup = effects({
displayData() {
renderTable(app.data);
},
showLoadingSpinner() {
const spinner = document.getElementById('spinner');
spinner.style.display = app.loading ? 'block' : 'none';
},
displayError() {
const errorBox = document.getElementById('error');
errorBox.textContent = app.error || '';
errorBox.style.display = app.error ? 'block' : 'none';
}
});
// Later, stop all effects at once
cleanup();Component Lifecycle Management
Perfect for component creation and disposal:
function createUserCard(userData) {
const user = state(userData);
const cleanup = effects({
displayName() {
document.getElementById('cardName').textContent = user.name;
},
displayEmail() {
document.getElementById('cardEmail').textContent = user.email;
},
displayAvatar() {
const img = document.getElementById('cardAvatar');
img.src = user.avatar;
},
displayStatus() {
const status = document.getElementById('cardStatus');
status.className = user.isOnline ? 'online' : 'offline';
status.textContent = user.isOnline ? 'Online' : 'Offline';
}
});
return {
state: user,
destroy: cleanup
};
}
// Create component
const card = createUserCard({
name: 'John',
email: 'john@example.com',
avatar: 'avatar.jpg',
isOnline: true
});
// Update data - all effects automatically update
card.state.isOnline = false;
// Destroy component - all effects stopped
card.destroy();Real-World Examples
Example 1: Complete Dashboard
const dashboard = state({
stats: {
users: 1250,
revenue: 45000,
orders: 320,
growth: 12.5
},
timeRange: 'week',
lastUpdate: new Date()
});
const cleanup = effects({
displayUserCount() {
document.getElementById('userCount').textContent =
dashboard.stats.users.toLocaleString();
},
displayRevenue() {
document.getElementById('revenue').textContent =
`$${dashboard.stats.revenue.toLocaleString()}`;
},
displayOrders() {
document.getElementById('orders').textContent =
dashboard.stats.orders.toLocaleString();
},
displayGrowth() {
const element = document.getElementById('growth');
element.textContent = `${dashboard.stats.growth}%`;
if (dashboard.stats.growth > 10) {
element.className = 'growth positive';
element.innerHTML = `📈 ${dashboard.stats.growth}%`;
} else if (dashboard.stats.growth > 0) {
element.className = 'growth neutral';
element.innerHTML = `➡️ ${dashboard.stats.growth}%`;
} else {
element.className = 'growth negative';
element.innerHTML = `📉 ${dashboard.stats.growth}%`;
}
},
displayTimeRange() {
document.querySelectorAll('.time-range').forEach(btn => {
btn.classList.toggle('active', btn.dataset.range === dashboard.timeRange);
});
},
displayLastUpdate() {
document.getElementById('lastUpdate').textContent =
`Last updated: ${dashboard.lastUpdate.toLocaleTimeString()}`;
}
});
// Update dashboard
setInterval(() => {
dashboard.stats.users += Math.floor(Math.random() * 10);
dashboard.stats.revenue += Math.floor(Math.random() * 1000);
dashboard.stats.orders += Math.floor(Math.random() * 5);
dashboard.lastUpdate = new Date();
}, 5000);Example 2: Multi-Step Form
const wizard = state({
currentStep: 1,
totalSteps: 3,
data: {
step1: { name: '', email: '' },
step2: { address: '', city: '' },
step3: { plan: '', payment: '' }
},
isValid: false
});
effects({
updateProgressBar() {
const progress = (wizard.currentStep / wizard.totalSteps) * 100;
document.getElementById('progress').style.width = `${progress}%`;
},
updateStepIndicator() {
document.getElementById('stepIndicator').textContent =
`Step ${wizard.currentStep} of ${wizard.totalSteps}`;
},
showCorrectStep() {
document.querySelectorAll('.step').forEach((step, index) => {
step.style.display = (index + 1) === wizard.currentStep ? 'block' : 'none';
});
},
updateNavigationButtons() {
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const submitBtn = document.getElementById('submitBtn');
prevBtn.disabled = wizard.currentStep === 1;
nextBtn.style.display = wizard.currentStep < wizard.totalSteps ? 'inline' : 'none';
submitBtn.style.display = wizard.currentStep === wizard.totalSteps ? 'inline' : 'none';
submitBtn.disabled = !wizard.isValid;
},
saveToLocalStorage() {
localStorage.setItem('wizardData', JSON.stringify(wizard.data));
localStorage.setItem('wizardStep', wizard.currentStep.toString());
}
});
// Navigation
function nextStep() {
if (wizard.currentStep < wizard.totalSteps) {
wizard.currentStep++;
}
}
function prevStep() {
if (wizard.currentStep > 1) {
wizard.currentStep--;
}
}Example 3: Real-Time Chat Interface
const chat = state({
messages: [],
onlineUsers: [],
currentUser: { id: 1, name: 'John' },
isTyping: false,
unreadCount: 0
});
effects({
renderMessages() {
const container = document.getElementById('messages');
container.innerHTML = chat.messages.map(msg => `
<div class="message ${msg.userId === chat.currentUser.id ? 'own' : 'other'}">
<strong>${msg.userName}:</strong> ${msg.text}
<span class="time">${new Date(msg.timestamp).toLocaleTimeString()}</span>
</div>
`).join('');
// Auto-scroll to bottom
container.scrollTop = container.scrollHeight;
},
displayOnlineUsers() {
const list = document.getElementById('onlineUsers');
list.innerHTML = chat.onlineUsers.map(user => `
<div class="user">
<span class="status-dot online"></span>
${user.name}
</div>
`).join('');
},
showTypingIndicator() {
const indicator = document.getElementById('typing');
indicator.style.display = chat.isTyping ? 'block' : 'none';
},
updateUnreadBadge() {
const badge = document.getElementById('unreadBadge');
badge.textContent = chat.unreadCount;
badge.style.display = chat.unreadCount > 0 ? 'inline' : 'none';
if (chat.unreadCount > 0) {
document.title = `(${chat.unreadCount}) New Messages - Chat`;
} else {
document.title = 'Chat';
}
},
playNotificationSound() {
if (chat.messages.length > 0) {
const lastMessage = chat.messages[chat.messages.length - 1];
if (lastMessage.userId !== chat.currentUser.id) {
playSound('notification.mp3');
chat.unreadCount++;
}
}
}
});
// Simulate receiving messages
function receiveMessage(msg) {
chat.messages.push(msg);
}Example 4: Settings Panel
const settings = state({
appearance: {
theme: 'light',
fontSize: 14,
compactMode: false
},
notifications: {
email: true,
push: true,
sound: false
},
privacy: {
profileVisibility: 'public',
showEmail: false
}
});
effects({
applyTheme() {
document.body.className = `theme-${settings.appearance.theme}`;
},
applyFontSize() {
document.documentElement.style.fontSize = `${settings.appearance.fontSize}px`;
},
applyCompactMode() {
document.body.classList.toggle('compact', settings.appearance.compactMode);
},
syncEmailNotifications() {
console.log(`Email notifications: ${settings.notifications.email ? 'ON' : 'OFF'}`);
updateServerSettings('email_notifications', settings.notifications.email);
},
syncPushNotifications() {
if (settings.notifications.push) {
requestPushPermission();
} else {
disablePushNotifications();
}
},
updateNotificationSound() {
audioContext.muted = !settings.notifications.sound;
},
saveToLocalStorage() {
localStorage.setItem('settings', JSON.stringify(settings));
},
logSettingsChange() {
console.log('Settings updated:', new Date().toISOString());
trackAnalytics('settings_changed', settings);
}
});
// Load saved settings
const saved = localStorage.getItem('settings');
if (saved) {
Object.assign(settings, JSON.parse(saved));
}Common Pitfalls
Pitfall #1: Effects with Same Dependencies
❌ Inefficient:
const user = state({ name: 'John' });
effects({
logName() {
console.log(user.name);
},
displayName() {
document.getElementById('name').textContent = user.name;
},
alertName() {
showToast(user.name);
}
});
// All three re-run when name changes✅ Better:
const user = state({ name: 'John' });
effects({
handleNameChange() {
// Group related actions together
console.log(user.name);
document.getElementById('name').textContent = user.name;
showToast(user.name);
}
});
// Single effect, one re-runWhat's happening:
- If multiple effects depend on the same data and do related things, consider combining them
- This reduces the number of re-runs
- However, keep effects separate if they handle different concerns
Pitfall #2: Not Saving Cleanup Function
❌ Wrong:
function createComponent() {
const state = ReactiveUtils.state({ value: 0 });
// Create effects but don't save cleanup
effects({
updateUI() {
renderUI(state.value);
}
});
return state;
}
// Memory leak: effects never cleaned up✅ Correct:
function createComponent() {
const state = ReactiveUtils.state({ value: 0 });
const cleanup = effects({
updateUI() {
renderUI(state.value);
}
});
return {
state,
destroy: cleanup
};
}What's happening:
- Always save the cleanup function
- Call it when the component/feature is destroyed
- This prevents memory leaks
Pitfall #3: Too Many Effects
❌ Inefficient:
effects({
effect1() { /* ... */ },
effect2() { /* ... */ },
effect3() { /* ... */ },
// ... 50 more effects
});
// Hard to manage and understand✅ Better:
// Group logically
const uiEffects = effects({
updateDisplay() { /* ... */ },
updateStyles() { /* ... */ }
});
const dataEffects = effects({
syncToServer() { /* ... */ },
validateInput() { /* ... */ }
});
// Easier to manageWhat's happening:
- If you have many effects, group them logically
- Create multiple
effects()calls for different concerns - Makes code more organized and maintainable
Summary
What is effects()?
effects() creates multiple named reactive effects from an object definition, providing organized effect creation and unified cleanup.
Why use effects() instead of multiple effect() calls?
- Organized, grouped effect creation
- Descriptive names for each effect
- Single cleanup function for all
- More readable code
- Easier maintenance
- Better debugging
Key Points to Remember:
1️⃣ Creates multiple effects at once - From an object definition
2️⃣ Each function becomes an effect - Runs immediately and on changes
3️⃣ Returns single cleanup - Stops all effects with one call
4️⃣ Descriptive names help - Self-documenting and aids debugging
5️⃣ Each effect is independent - Tracks its own dependencies
6️⃣ Perfect for components - Organize all component effects together
7️⃣ Always clean up - Call the returned function when done
Mental Model: Think of effects() like an organized checklist - it groups related reactive tasks together with descriptive names and provides a single way to clean up everything at once.
Quick Reference:
// Create state
const app = state({
user: 'John',
theme: 'light',
count: 0
});
// Create multiple named effects
const cleanup = effects({
displayUser() {
console.log(`User: ${app.user}`);
},
applyTheme() {
document.body.className = app.theme;
},
updateCounter() {
document.getElementById('count').textContent = app.count;
}
});
// All run immediately and update automatically
app.user = 'Jane';
app.theme = 'dark';
app.count = 5;
// Stop all effects at once
cleanup();Remember: effects() is your tool for creating organized, named groups of reactive effects. It makes code more maintainable and provides clean, unified cleanup! ✨