Skip to content

Sponsor

Sponsor

Understanding effects() - A Beginner's Guide

Quick Start (30 seconds)

Need to create multiple related effects at once? Here's how:

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

js
// 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:

js
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:

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

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

Key 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):

js
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:

js
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 automatically

Each Effect is Independent

Each effect tracks its own dependencies:

js
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-runs

Organizing Multiple Effects

js
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

js
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:

js
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 name

Self-Documenting Code

Names explain what each effect does:

js
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 code

Cleanup and Disposal

Single Cleanup for All Effects

The main advantage of effects() is unified cleanup:

js
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:

js
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

js
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

js
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

js
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

js
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:

js
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:

js
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-run

What'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:

js
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:

js
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:

js
effects({
  effect1() { /* ... */ },
  effect2() { /* ... */ },
  effect3() { /* ... */ },
  // ... 50 more effects
});

// Hard to manage and understand

Better:

js
// Group logically
const uiEffects = effects({
  updateDisplay() { /* ... */ },
  updateStyles() { /* ... */ }
});

const dataEffects = effects({
  syncToServer() { /* ... */ },
  validateInput() { /* ... */ }
});

// Easier to manage

What'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:

js
// 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! ✨