Chapter 10.3☕ 15 min read

Effect — Side Effects with effect()

effect() runs a function whenever signals it reads change. Use it for side effects: logging, localStorage, API calls, DOM manipulation, syncing with non-Angular libraries.

01What is effect()

effect() is a function that runs a callback whenever the signals it reads change. Think of it as a "watcher" — when signal A changes, do action B.

"Effect = watcher — signal badla toh kuch karo automatically."

import { signal, effect } from '@angular/core';

const count = signal(0);

// Register an effect
effect(() => {
  console.log('Count changed to:', count());
  localStorage.setItem('count', count().toString());
});

// When we change the signal, the effect runs automatically
count.set(5);
// Console: "Count changed to: 5"
// localStorage: "count" = "5"

Key characteristics of effect():

  • Runs automatically when registered — and again whenever read signals change
  • Tracks signals automatically — any signal you read inside effect() becomes a dependency
  • Cleaned up automatically — when the component is destroyed, effects are destroyed too
  • Runs during change detection — not immediately, but before Angular renders

⚠️ IMPORTANT: effect() is for SIDE EFFECTS, NOT for deriving values. If you need a value to use in template, use computed(). Effect does NOT return a value.

02Creating an Effect

Creating an effect is simple — call effect() in the component's constructor or field initializer.

import { Component, signal, effect } from '@angular/core';

@Component({...})
export class SettingsComponent {
  theme = signal('dark');
  fontSize = signal(16);

  // Effect in field initializer
  private saveSettings = effect(() => {
    localStorage.setItem('theme', this.theme());
    localStorage.setItem('fontSize', this.fontSize().toString());
    console.log('Settings saved:', this.theme(), this.fontSize());
  });

  changeTheme(newTheme: string) {
    this.theme.set(newTheme);
    // → effect runs: saves to localStorage, logs
  }
}

Angular automatically tracks which signals you read inside the effect. When theme() or fontSize() changes, the effect re-runs.

"effect automatically tracks which signals you read inside it — jitne bhi signals padho, sab dependencies ban jayenge."

Effect runs in injection context: effect() must be called in an injection context (constructor, field initializer, or a function called from one). This is because it needs access to DestroyRef for auto-cleanup.

03Effect vs Computed — When to Use What

This is the most important distinction in signals: computed() is for values, effect() is for actions.

📊 computed vs effect — The Golden Rule

computed()effect()
Returns✅ A value (Signal)❌ Nothing (void)
PurposeDerive statePerform side effects
Pure?✅ Yes — no side effects❌ No — side effects expected
Used in template✅ Yes: {{ fullName() }}❌ No
Runs whenDependency changes + someone reads itDependency changes
PerformanceCached, lazyRuns eagerly
// ✅ CORRECT: computed for deriving values
total = computed(() => price() * quantity());
// Template: {{ total() }}

// ✅ CORRECT: effect for side effects
effect(() => localStorage.setItem('total', this.total().toString()));

// ❌ WRONG: computed with side effect
bad = computed(() => {
  localStorage.setItem('count', this.count().toString()); // Side effect!
  return this.count() * 2;
});

// ❌ WRONG: effect for deriving value
effect(() => {
  this.double = this.count() * 2; // Should use computed!
});

"computed = calculator (returns value), effect = alarm (does action)."

Rule: If you need a return value → computed(). If you need to DO something → effect(). Never use effect() to set another signal — use computed() or .update() instead.

04Cleanup in Effect

effects can have cleanup — a function that runs BEFORE the effect re-runs and when the component is destroyed.

import { effect } from '@angular/core';

effect((onCleanup) => {
  const timer = setInterval(() => {
    console.log('Tick:', count());
  }, 1000);

  // Cleanup function — runs before next effect run and on destroy
  onCleanup(() => {
    clearInterval(timer);
    console.log('Timer cleaned up');
  });
});

"Cleanup = mopping — pehle purana saaf karo, phir naya lagao."

Why cleanup matters:

  • Prevents memory leaks — intervals, subscriptions, event listeners
  • Prevents stale data — old API response vs new response
  • Prevents multiple instances — count changes sets interval again without clearing old one
// Without cleanup — BUG!
effect(() => {
  setInterval(() => console.log(count()), 1000);
  // Every time count changes, NEW interval starts!
  // Old ones keep running — memory leak!
});

// With cleanup — CORRECT!
effect((onCleanup) => {
  const id = setInterval(() => console.log(count()), 1000);
  onCleanup(() => clearInterval(id));
  // Old interval is cleared before new one starts
});
05Practical Use Cases

Here are real-world use cases where effect() shines:

1. Auto-save to localStorage

effect(() => {
  const settings = this.settings();
  localStorage.setItem('app-settings', JSON.stringify(settings));
});

2. Sync with non-Angular library (Chart.js, Leaflet)

chartEffect = effect(() => {
  const data = this.chartData();
  this.chartInstance.data.datasets[0].data = data;
  this.chartInstance.update(); // Re-render chart
});

3. Console logging for debugging

logEffect = effect(() => {
  console.log('🔄 User:', this.user(), '| Theme:', this.theme());
});

4. Show toast notification

notificationEffect = effect(() => {
  const err = this.errorMessage();
  if (err) {
    this.toastService.show(err, { duration: 3000 });
  }
});

5. Document title update

titleEffect = effect(() => {
  document.title = `${this.pageTitle()} — BiryaniHub`;
});

"effect = bridge between Angular signals and outside world — localStorage, DOM, libraries, APIs — sab kuch."

⚠️ Don't overuse effect! If a template can handle it (show/hide, class toggle), don't use effect. Effects are for IMPERATIVE actions that the template can't handle declaratively.

Key Takeaways

  • ✅ effect() runs a callback when signals it reads change — for side effects
  • ✅ NOT for deriving values — use computed() for that
  • ✅ Auto-tracks dependencies — signals read inside effect() are tracked
  • ✅ onCleanup() — runs before effect re-runs and on component destroy
  • ✅ Use cases: localStorage, logging, DOM updates, library sync
  • ✅ DON'T write to signals inside effect that are part of the same dependency chain