Computed & Effect (v16)

📚 Computed and Effect

computed() - Derived state that auto-updates:

  • Memoized - only recalculates when dependencies change
  • Read-only - cannot be set directly
  • Lazy - only calculates when read

effect() - Side effects when signals change:

  • Runs when tracked signals change
  • Great for logging, localStorage, external APIs
  • Auto-cleanup on destroy

🎯 Interview Questions

  • Q1: What is the difference between computed() and effect()?
  • A: computed() creates derived read-only values. effect() runs side effects when signals change but returns nothing.
  • Q2: Is computed() lazy or eager?
  • A: Lazy - it only calculates when the value is read, not when dependencies change.
  • Q3: Can you write to signals inside effect()?
  • A: By default no (to prevent infinite loops). Use allowSignalWrites: true or untracked() to do it safely.
  • Q4: What is untracked()?
  • A: Reads signal value without creating dependency. Useful in effects when you don't want to track certain signals.

🔧 Live Demo - computed()

Shopping Cart Example

Subtotal: $59.98(computed: price × quantity)

Tax (8%): $4.80(computed: subtotal × 0.08)

Total: $64.78(computed: subtotal + tax)

Filtered List (computed)

  • Apple
  • Banana
  • Cherry
  • Date
  • Elderberry
  • Fig
  • Grape

Found: 7 of 7

🔧 Live Demo - effect()

Effect Logger

Theme: light

Effect Log (check console too):

  • [12:07:42 PM] Theme changed to: light

💻 Computed & Effect Code


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

// ===== computed() - Derived State =====

const price = signal(10);
const quantity = signal(2);

// Computed automatically recalculates when dependencies change
const total = computed(() => price() * quantity());

console.log(total()); // 20
price.set(15);
console.log(total()); // 30 (auto-updated!)

// Computed is read-only
// total.set(100); // ❌ Error!

// ===== effect() - Side Effects =====

const theme = signal<'light' | 'dark'>('light');

// Effect runs when theme signal changes
effect(() => {
  const currentTheme = theme();
  console.log('Theme changed to:', currentTheme);
  localStorage.setItem('theme', currentTheme);
  document.body.className = currentTheme;
});

theme.set('dark'); // Effect runs automatically!

// ===== untracked() - Read Without Tracking =====

const firstName = signal('John');
const lastName = signal('Doe');
const logCount = signal(0);

effect(() => {
  // This effect only tracks firstName
  const first = firstName();
  
  // Read lastName without tracking (won't re-run if lastName changes)
  const last = untracked(() => lastName());
  
  console.log(`Name: ${first} ${last}`);
});

// ===== Effect Cleanup =====

effect((onCleanup) => {
  const interval = setInterval(() => {
    console.log('Tick:', Date.now());
  }, 1000);
  
  // Cleanup when effect re-runs or component destroys
  onCleanup(() => clearInterval(interval));
});

// ===== Effect Options =====

effect(() => { ... }, {
  allowSignalWrites: true, // Allow writing to signals (careful!)
  injector: this.injector, // Custom injector
});

// ===== Creating Effect Outside Constructor =====

// In a method (need injector)
const injector = inject(Injector);

someMethod() {
  runInInjectionContext(this.injector, () => {
    effect(() => {
      console.log('This works!');
    });
  });
}