| |

Angular 49 🅰️ Effects and Side Effects

An effect is a function that runs when the signals it reads change. It is the escape hatch from the signal graph to the imperative world — the place where a signal’s change is translated into a side effect that is not a value: a DOM update, a log, a persistence write, an external API call. The effect is the third of the three signal primitives, and it is the one that is easiest to misuse. The Angular team’s guidance is explicit: use computed for the derived values, and use effect only for the side effects that cannot be expressed as a derived value. An effect that fetches data, that writes to a signal it reads, or that computes a value is a mistake, and the mistakes produce loops, redundant requests, and unpredictable behavior. This chapter covers the effect in detail: how it works, when it runs, the cleanup mechanism, the difference between an effect and a subscription, the cases where an effect is the right tool, and the pitfalls that make it the wrong one.

Key point: The effect(fn) function runs fn immediately and re-runs it when any signal read inside fn changes. The effect tracks its dependencies the same way a computed does — the reads are recorded — but the effect does not produce a value. It is for side effects: the DOM, the logs, the storage, the external calls. The effect’s fn can accept an onCleanup callback that registers a cleanup function, which runs before the next execution and when the effect is destroyed. The effect runs after the change detection, and it is scheduled on the microtask queue. It is not a replacement for computed, for the RxJS pipelines, or for the data fetching. The effect is the tool of last resort, and the Angular team recommends using it sparingly.


What an effect is

An effect is a function that runs when the signals it reads change. It is created with the effect function, and it registers itself with the current injection context.

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

const count = signal(0);

effect(() => {
  console.log('count is', count());
});
// count is 0

count.set(1);
// count is 1

count.set(2);
// count is 2

The effect runs immediately with the initial value, then re-runs on each change. The read of count() is recorded, and the effect is registered as a consumer.

Why the effect runs immediately. The immediate run is the initial side effect — the log, the DOM setup, the initial registration. The effect’s function runs once at creation, then on each dependency change. The immediate run is what makes the effect useful for the setup, not just the reaction.

Why the effect tracks the signals it reads. The reads inside the effect’s function are recorded, and the dependency list is the set of signals read. A conditional read — a signal read only in one branch — is a dependency only when the branch runs, the same as a computed. The dynamic dependency list is the model.

Why the effect does not produce a value. Unlike a computed, an effect has no return value that the consumer reads. The effect’s purpose is the side effect, and the side effect is the observable result — the log, the DOM update, the external call. The effect is not a signal, and it cannot be read.

Why the effect is created in an injection context. The effect function uses inject() internally to get the DestroyRef, and it must be called during the component’s construction or in a field initializer. An effect created outside an injection context is an error, and the fix is to create it in the constructor or to pass a manual injector.

Why the effect’s lifetime is tied to the context. The effect is destroyed when its injection context is destroyed, which is when the component or the service is destroyed. The cleanup functions run on destroy, and the effect is released. The automatic lifetime is what makes the effect safe — no manual unsubscribe, no leak.

Why the effect is the modern replacement for the ngOnChanges and the subscribe patterns. The old patterns were to react to input changes with ngOnChanges and to react to service streams with a subscribe in the constructor. The effect is the modern answer for both: the inputs are signals, and the service state is a signal. The effect reacts to the signal changes, and the cleanup is automatic.


When the effect runs

The effect’s timing is not the same as a signal’s change. The effect runs after the change detection and on the microtask queue, which is a deliberate design.

The immediate run. The effect runs once at creation, with the current values of the signals. The immediate run is the initial side effect, and it is the same as any other run.

The re-run on change. When a signal the effect reads changes, the effect is scheduled for a re-run. The scheduling is not synchronous with the signal’s change; the effect runs after the current change detection cycle.

Why the effect runs after the change detection. The effect’s side effect — a DOM update, a log — should see the state after the change detection has settled. Running the effect synchronously with the signal’s change would interleave the side effect with the change detection, which is unpredictable. The deferred run is the safer timing.

Why the effect is on the microtask queue. The effect is scheduled as a microtask, which runs after the current synchronous execution and before the next macrotask. The timing is after the change detection, and it is the earliest safe moment.

Why the effect’s re-run is coalesced. Multiple signal changes in the same synchronous block schedule the effect once, not once per change. The effect’s re-run is coalesced, and the effect sees the final state.

count.set(1);
count.set(2);
count.set(3);
// The effect runs once, with count = 3

The three changes schedule one re-run, and the effect sees the final value. The coalescing is the model.

Why the effect’s re-run can be scheduled multiple times. The effect re-runs when a signal changes, and the re-run may change another signal that the effect reads, which schedules another re-run. The chain of re-runs is the effect’s own behavior, and it can loop if the effect writes to a signal it reads.

Why the effect’s timing matters for the DOM. A DOM read after a signal’s change should see the updated DOM, which is why the effect runs after the change detection. The DOM read before the change detection would see the stale DOM. The timing is the correctness.

Why the effect’s timing matters for the external calls. An external call — an API, a log, a persistence write — should happen after the state is settled, which is why the effect runs after the change detection. A call during the change detection would interleave with the state’s updates, which is unpredictable. The deferred timing is the safety.


The cleanup mechanism

The effect’s function can accept an onCleanup callback that registers a cleanup function. The cleanup runs before the next execution of the effect and when the effect is destroyed.

effect((onCleanup) => {
  const timer = setInterval(() => console.log('tick'), 1000);
  onCleanup(() => clearInterval(timer));
});

The effect creates a timer, and the cleanup clears it. The timer is released before the next run, and the leak is prevented.

Why the cleanup is needed. An effect that creates a resource — a timer, an event listener, an external subscription — must release the resource. Without the cleanup, the resource accumulates on each re-run, and the leak grows.

Why the cleanup runs before the next execution. The cleanup runs before the effect’s next run, which releases the previous resource before the new one is created. The pattern is the same as the Observable’s teardown, and it is the mechanism for the effects that create resources.

Why the cleanup runs on destroy. The cleanup runs when the effect is destroyed, which is when the injection context is destroyed. The resource is released, and the leak is prevented. The automatic cleanup is what makes the effect safe.

Why the effect can register multiple cleanups. The onCleanup can be called multiple times, and each registered function runs on the cleanup. The multiple cleanups are for the effects that create multiple resources, and each is released independently.

effect((onCleanup) => {
  const timer1 = setInterval(() => {}, 1000);
  const timer2 = setInterval(() => {}, 2000);
  onCleanup(() => clearInterval(timer1));
  onCleanup(() => clearInterval(timer2));
});

Why the cleanup should be idempotent. The cleanup may run more than once in some edge cases, and it should be safe to run again. The cleanup that clears a timer is idempotent — clearing an already-cleared timer is a no-op. The cleanup that removes an event listener is the same. The idempotency is the safety.

Why the cleanup should not throw. A cleanup that throws breaks the effect’s lifecycle. The cleanup should be defensive, and any error within it should be handled locally. An error in the cleanup is worse than the resource leak it was meant to prevent.

Why the cleanup is the replacement for the ngOnDestroy pattern. The old pattern was to create the resource in ngOnInit and release it in ngOnDestroy. The effect’s cleanup is the modern version: the resource is created in the effect, and the cleanup is registered with onCleanup. The two are tied, and the release is automatic.


The effect vs the subscription

The effect and the RxJS subscription both run a function when a value changes. The difference is in the model and the timing.

AspectEffectSubscription
SourceSignalsObservables
TimingAfter change detection, microtaskSynchronous on emission
DependenciesAutomaticThe observable
CleanuponCleanupunsubscribe or takeUntil
LifetimeInjection contextManual
ValuesThe current signal valuesThe emitted values
UseSide effects on signal changeStream processing

Why the effect is not a subscription. The effect is not subscribed to a stream; it is scheduled to re-run when a signal changes. The signal is a value, and the effect reads it. The subscription is for the streams, which the effect does not handle.

Why the effect’s timing is different. The subscription runs synchronously with the emission. The effect runs after the change detection. The two timings are different, and the difference matters for the side effects that depend on the DOM state.

Why the effect’s dependencies are automatic. The effect’s dependencies are the signals read, which are recorded. The subscription’s dependency is the observable, which is explicit. The automatic tracking is the effect’s advantage for the signal-based code.

Why the effect’s cleanup is simpler. The onCleanup is registered once, and the cleanup runs automatically. The subscription’s cleanup requires an unsubscribe or a takeUntil, which must be written. The effect’s cleanup is less error-prone.

Why the effect is not a replacement for the subscription. The streams — HTTP, WebSocket, intervals — are Observables, and the operators are the tool for the stream processing. The effect is for the signals, not the streams. The two are complementary, and the toSignal and toObservable are the bridges.

Why the effect and the subscription can coexist. A component can have both: a subscription to an Observable for the stream processing and an effect for the signal-based side effect. The two are separate, and the two coexist. The discipline is to use each for the right purpose.

Why the effect is the modern pattern for the signal-based code. The signal-based state is read in the template, derived in the computed, and reacted to in the effect. The subscription is for the streams. The two patterns are separate, and the effect is the modern one for the signals.


When to use an effect

The effect is for the side effects that cannot be expressed as a derived value. The list is short, and the effect should be used only for these.

The DOM update. An effect that updates the DOM directly — setting a class, adjusting a style, focusing an element — is the effect’s use case. The DOM is not a value, and the computed cannot express it.

effect(() => {
  document.body.classList.toggle('dark', this.theme() === 'dark');
});

The persistence. An effect that persists a state to localStorage, sessionStorage, or a cookie is the effect’s use case. The persistence is a side effect, and the computed cannot express it.

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

The logging. An effect that logs a state change — to the console, to an analytics service, to a log aggregator — is the effect’s use case. The log is a side effect, and the computed cannot express it.

effect(() => {
  this.analytics.track('filter_changed', { filter: this.filter() });
});

The external integration. An effect that calls an external API — a charting library, a mapping library, a third-party widget — is the effect’s use case. The external library is imperative, and the effect bridges the signal to the imperative call.

effect(() => {
  this.chart.update(this.data());
});

The initialization. An effect that performs an initial action — a one-time setup, a registration — is the effect’s use case. The immediate run is the initial action.

effect(() => {
  this.service.register(this.config());
});

Why the list is short. The list is the cases where the side effect is genuinely imperative and not a value. The Angular team’s guidance is to use the effect sparingly, and the short list is the reason. An effect that does more than the list is a candidate for a different mechanism.

Why the effect should not fetch data. The data fetching is not a side effect; it is an asynchronous operation that produces a value. The switchMap of the signal’s stream is the tool, or the resource API. The effect that fetches produces a request on every dependency change, which is the wrong behavior.

Why the effect should not compute a value. The value is a computed. The effect that computes a value and writes it to a signal is the wrong pattern — the computed is the derived value, and the signal is the state. The computed is the tool.

Why the effect should not write to a signal it reads. The write triggers the signal’s change, which schedules the effect’s re-run, which writes again. The loop is the mistake. If the write is necessary, the target signal should be different from the source, and the effect’s re-run should not re-trigger itself.

Why the effect should not be the default. The default for a derived value is the computed. The default for a state change is the signal’s set or update. The effect is the exception, and the exceptions should be rare. The signal-based code that uses the effect as the default is the code that is fighting the model.


The pitfalls

The effect’s pitfalls are the reason the Angular team recommends using it sparingly. Each is a common mistake, and each produces a bug.

The loop. An effect that writes to a signal it reads creates a loop. The signal’s change schedules the effect, the effect writes to the signal, the signal’s change schedules the effect again. The loop is infinite, and the fix is to break the write — use a computed, or write to a different signal.

// Wrong
effect(() => {
  this.count.set(this.count() + 1);  // ❌ loop
});

The redundant request. An effect that fetches data produces a request on every dependency change. The request is redundant when the dependency changes for an unrelated reason, and the fetch is the wrong tool. The switchMap of the signal’s stream is the fix.

// Wrong
effect(() => {
  this.http.get(`/api/items?q=${this.filter()}`).subscribe((items) => {
    this.items.set(items);
  });
});

The incorrect timing. The effect runs after the change detection, which is not the same as synchronously with the signal’s change. An effect that expects the synchronous timing is surprised by the deferred run. The timing is the model, and the effect should be written for it.

The missed dependency. The effect reads a signal conditionally, and the dependency is only present when the branch runs. The effect re-runs only when the current dependency list changes, which is correct but can be surprising.

effect(() => {
  if (this.mode() === 'dark') {
    console.log(this.darkColor());  // dependency only in dark mode
  }
});

The cleanup missed. An effect that creates a resource without the cleanup leaks the resource. The cleanup is the mechanism, and the missing cleanup is the mistake.

The effect created outside the injection context. The effect must be created in an injection context, and the creation outside the context is an error. The fix is to create it in the constructor or to pass a manual injector.

The effect that is the only consumer of a signal. A signal that is read only by an effect does not mark the component for check, which is correct — the effect is the consumer, and the component is not. The signal is used for the side effect, and the template does not read it. The pattern is correct, and the surprise is the component that expects the signal to mark it.

The effect that is used for the derived value. An effect that computes a value and writes it to a signal is the wrong pattern. The computed is the derived value, and the effect is the side effect. The confusion is the mistake.

Why the pitfalls are the reason for the guidance. The effect is powerful, and the power is easy to misuse. The guidance — use computed for the derived values, use the effect only for the side effects — is the discipline that prevents the pitfalls. The effect is the escape hatch, and the escape hatch should be used rarely.


Complete Example Session

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

// ============================================
// PART 1: THE BASIC EFFECT
// ============================================

const count = signal(0);

effect(() => {
  console.log('count is', count());
});
// count is 0

count.set(1);
// count is 1

// ============================================
// PART 2: THE EFFECT WITH CLEANUP
// ============================================

@Component({ selector: 'app-clock', standalone: true, template: `{{ time() }}` })
export class ClockComponent {
  readonly time = signal(new Date().toLocaleTimeString());

  constructor() {
    effect((onCleanup) => {
      const timer = setInterval(() => {
        this.time.set(new Date().toLocaleTimeString());
      }, 1000);
      onCleanup(() => clearInterval(timer));
    });
  }
}

// ============================================
// PART 3: THE DOM EFFECT
// ============================================

@Component({ selector: 'app-theme', standalone: true, template: `` })
export class ThemeComponent {
  private readonly theme = inject(ThemeService);

  constructor() {
    effect(() => {
      document.body.classList.toggle('dark', this.theme.current() === 'dark');
    });
  }
}

// ============================================
// PART 4: THE PERSISTENCE EFFECT
// ============================================

@Component({ selector: 'app-settings', standalone: true, template: `` })
export class SettingsComponent {
  private readonly settings = inject(SettingsService);

  constructor() {
    effect(() => {
      localStorage.setItem('settings', JSON.stringify(this.settings.current()));
    });
  }
}

// ============================================
// PART 5: THE LOGGING EFFECT
// ============================================

@Component({ selector: 'app-search', standalone: true, template: `` })
export class SearchComponent {
  private readonly analytics = inject(AnalyticsService);
  readonly filter = signal('');

  constructor() {
    effect(() => {
      this.analytics.track('filter_changed', { filter: this.filter() });
    });
  }
}

// ============================================
// PART 6: THE EXTERNAL INTEGRATION
// ============================================

@Component({ selector: 'app-chart', standalone: true, template: `<canvas #canvas></canvas>` })
export class ChartComponent {
  private readonly chart = inject(ChartService);
  readonly data = signal<number[]>([]);

  constructor() {
    effect(() => {
      this.chart.update(this.data());
    });
  }
}

// ============================================
// PART 7: THE EFFECT THAT WRITES TO A DIFFERENT SIGNAL
// ============================================

@Component({ selector: 'app-mirror', standalone: true, template: `` })
export class MirrorComponent {
  readonly source = signal(0);
  readonly mirrored = signal(0);

  constructor() {
    effect(() => {
      this.mirrored.set(this.source() * 2);  // different signal, no loop
    });
  }
}

// ============================================
// PART 8: THE PITFALL — THE LOOP
// ============================================

// effect(() => {
//   this.count.set(this.count() + 1);  // ❌ infinite loop
// });

// ============================================
// PART 9: THE PITFALL — THE FETCH
// ============================================

// effect(() => {
//   this.http.get(`/api/items?q=${this.filter()}`).subscribe((items) => {
//     this.items.set(items);
//   });
// });
// ❌ use switchMap in a pipeline instead

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't write to a signal the effect reads
// effect(() => { this.count.set(this.count() + 1); });  // loop

// Don't fetch data in an effect
// Use switchMap or the resource API.

// Don't compute a value in an effect
// Use a computed.

// Don't forget the cleanup
// effect((onCleanup) => { const t = setInterval(...); onCleanup(() => clearInterval(t)); });

// Don't create an effect outside the injection context
// Create it in the constructor.

// Don't use an effect as the default
// Use computed for the derived values.

The ten parts cover the basic effect, the cleanup, the DOM, the persistence, the logging, the external integration, the write to a different signal, the loop pitfall, the fetch pitfall, and the anti-patterns.


Quick Reference

The effect Function

FormPurpose
effect(() => { ... })Basic side effect
effect((onCleanup) => { ... })With cleanup

The Effect’s Timing

TimingValue
Immediate runAt creation
Re-runOn dependency change
ScheduleMicrotask, after change detection
CoalescingOne run per synchronous block

The Cleanup

PatternPurpose
onCleanup(fn)Register a cleanup
Multiple callsMultiple cleanups
On re-runRuns before the next run
On destroyRuns when the context is destroyed

When to Use an Effect

UseExample
DOM updatedocument.body.classList.toggle(...)
PersistencelocalStorage.setItem(...)
Logginganalytics.track(...)
External integrationchart.update(...)
Initializationservice.register(...)

When Not to Use an Effect

Do not use forUse instead
Derived valuecomputed
Data fetchingswitchMap or resource
State updateset or update
Stream processingRxJS operators
Template valueSignal read in the template

The Effect vs the Subscription

AspectEffectSubscription
SourceSignalsObservables
TimingAfter change detectionOn emission
CleanuponCleanupunsubscribe
LifetimeInjection contextManual
UseSide effects on signal changeStream processing

Best Practices

✅ Do This:

// Use an effect for a DOM update
effect(() => document.body.classList.toggle('dark', this.theme() === 'dark')); // ✅

// Use an effect for persistence
effect(() => localStorage.setItem('settings', JSON.stringify(this.settings()))); // ✅

// Use the cleanup for the resources
effect((onCleanup) => {
  const timer = setInterval(() => {}, 1000);
  onCleanup(() => clearInterval(timer));
});                                                            // ✅

// Use an effect for the external integration
effect(() => this.chart.update(this.data()));                  // ✅

// Use an effect for the logging
effect(() => this.analytics.track('filter', { filter: this.filter() })); // ✅

// Create the effect in the constructor
constructor() {
  effect(() => { ... });
}                                                              // ✅

// Use the computed for the derived value
readonly total = computed(() => this.items().reduce(...));     // ✅

❌ Don’t Do This:

// Don't write to a signal the effect reads
effect(() => { this.count.set(this.count() + 1); });           // ⚠️ loop

// Don't fetch data in an effect
effect(() => {
  this.http.get('/api').subscribe((d) => this.data.set(d));
});                                                            // ⚠️

// Don't compute a value in an effect
effect(() => { this.doubled.set(this.count() * 2); });         // ⚠️ use computed

// Don't forget the cleanup
effect(() => { setInterval(() => {}, 1000); });                // ⚠️ leak

// Don't create the effect outside the injection context
function createEffect() {
  effect(() => {});  // ❌ no injection context
}                                                              // ⚠️

// Don't use an effect as the default
// Use computed for the derived values.                        // ⚠️

// Don't expect the effect to run synchronously
// The effect runs after the change detection.                 // ⚠️

Common Pitfalls

PitfallProblemSolution
Effect writes to its sourceInfinite loopUse computed or a different signal
Effect fetches dataRedundant requestsUse switchMap
Effect computes a valueWrong toolUse computed
Missing cleanupResource leakUse onCleanup
Effect outside the injection contextCompile errorCreate in the constructor
Effect used as the defaultFights the modelUse computed
Synchronous expectationDeferred runWrite for the timing
Conditional dependency missedSurpriseKnow the dynamic list

Real-World Examples

1. Theme DOM update

effect(() => document.body.classList.toggle('dark', this.theme() === 'dark'));

2. Settings persistence

effect(() => localStorage.setItem('settings', JSON.stringify(this.settings())));

3. Analytics log

effect(() => this.analytics.track('filter', { filter: this.filter() }));

4. Chart update

effect(() => this.chart.update(this.data()));

5. Timer with cleanup

effect((onCleanup) => {
  const t = setInterval(() => this.time.set(Date.now()), 1000);
  onCleanup(() => clearInterval(t));
});

6. Title update

effect(() => this.title.setTitle(this.pageTitle()));

7. Focus management

effect(() => {
  if (this.dialogOpen()) this.dialogElement()?.focus();
});

8. Mirror signal

effect(() => this.mirrored.set(this.source() * 2));

9. Initialization

effect(() => this.service.register(this.config()));

10. Third-party widget

effect(() => this.widget.setData(this.data()));

Visual: The Effect

┌──────────────────────────────────────────────────────────┐
│  effect(() => {                                          │
│    const current = this.theme();                         │
│    document.body.classList.toggle('dark', current === 'dark');│
│  });                                                     │
│                                                          │
│  CREATION                                                │
│    The effect runs immediately.                          │
│    The read of this.theme() is recorded.                 │
│    The DOM is updated.                                   │
│                                                          │
│  CHANGE                                                  │
│    this.theme.set('dark')                                │
│       │                                                  │
│       ▼                                                  │
│    The signal notifies its consumers.                    │
│       │                                                  │
│       ▼                                                  │
│    The effect is scheduled (microtask, after change detection).│
│       │                                                  │
│       ▼                                                  │
│    The effect re-runs.                                   │
│    The DOM is updated.                                   │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Cleanup

┌──────────────────────────────────────────────────────────┐
│  effect((onCleanup) => {                                 │
│    const timer = setInterval(() => {}, 1000);            │
│    onCleanup(() => clearInterval(timer));                │
│  });                                                     │
│                                                          │
│  FIRST RUN                                               │
│    The timer is created.                                 │
│    The cleanup is registered.                            │
│                                                          │
│  DEPENDENCY CHANGE                                       │
│    The cleanup runs (the timer is cleared).              │
│    The effect re-runs (a new timer is created).          │
│                                                          │
│  DESTROY                                                 │
│    The cleanup runs (the timer is cleared).              │
│    The effect is released.                               │
│                                                          │
│  No leak.                                                │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Loop

┌──────────────────────────────────────────────────────────┐
│  effect(() => {                                          │
│    this.count.set(this.count() + 1);  ← writes the source│
│  });                                                     │
│                                                          │
│  The effect runs.                                        │
│       │                                                  │
│       ▼                                                  │
│  this.count changes.                                     │
│       │                                                  │
│       ▼                                                  │
│  The effect is scheduled.                                │
│       │                                                  │
│       ▼                                                  │
│  The effect runs.                                        │
│       │                                                  │
│       ▼                                                  │
│  this.count changes.                                     │
│       │                                                  │
│       ▼                                                  │
│  ... infinite loop                                       │
│                                                          │
│  Fix: use a computed, or write to a different signal.    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Right Tool

┌──────────────────────────────────────────────────────────┐
│  What do you need?                                       │
│       │                                                  │
│       ├── A derived value                                │
│       │      └── computed                                │
│       │                                                  │
│       ├── A state update                                 │
│       │      └── signal.set() or signal.update()         │
│       │                                                  │
│       ├── A data fetch                                   │
│       │      └── switchMap or resource                   │
│       │                                                  │
│       ├── A stream processing                            │
│       │      └── RxJS operators                          │
│       │                                                  │
│       └── A side effect (DOM, storage, log, external)    │
│              └── effect                                  │
│                                                          │
│  The effect is the last resort, not the default.         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Effect vs the Subscription

┌──────────────────────────────────────────────────────────┐
│  EFFECT                                                  │
│                                                          │
│  effect(() => {                                          │
│    console.log(this.count());                            │
│  });                                                     │
│                                                          │
│  Runs after the change detection.                        │
│  Cleanup via onCleanup.                                  │
│  Lifetime tied to the injection context.                 │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  SUBSCRIPTION                                            │
│                                                          │
│  count$.subscribe((value) => {                           │
│    console.log(value);                                   │
│  });                                                     │
│                                                          │
│  Runs synchronously on emission.                         │
│  Cleanup via unsubscribe or takeUntil.                   │
│  Lifetime managed manually.                              │
│                                                          │
│  The two are complementary, not interchangeable.         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

AspectValue
Created witheffect(fn)
RunsImmediately, then on dependency change
TimingMicrotask, after change detection
CleanuponCleanup(fn)
LifetimeInjection context
TracksThe signals read
ProducesNo value (side effect only)
Use an effect forUse something else for
DOM updatesDerived value → computed
PersistenceState update → set/update
LoggingData fetch → switchMap/resource
External integrationStream processing → RxJS
InitializationTemplate value → signal read

Key takeaways:

  • An effect runs a side effect when the signals it reads change — it is the escape hatch to the imperative world, and it is not a value
  • The effect runs immediately and re-runs on dependency change — the immediate run is the initial side effect, and the re-run is the reaction
  • The effect’s timing is after the change detection, on the microtask queue — the deferred timing is the safety for the DOM and the external calls
  • The onCleanup callback registers a cleanup — it runs before the next execution and on destroy, and it is the mechanism for the resources
  • The effect’s lifetime is tied to the injection context — the cleanup is automatic, and the leak is prevented
  • The effect is for the side effects that cannot be a derived value — the DOM, the persistence, the logging, the external integration
  • The effect should not be used for the derived values — the computed is the tool, and the effect that computes is a mistake
  • The effect should not fetch data — the switchMap of the signal’s stream or the resource API is the tool, and the effect that fetches produces redundant requests
  • The effect should not write to a signal it reads — the loop is the mistake, and the computed or a different signal is the fix
  • The effect is the last resort, not the default — the Angular team recommends using it sparingly, and the discipline is to reach for the computed first

Remember: An effect is the bridge from the signal graph to the imperative world. It runs when the signals it reads change, it cleans up after itself, and it is for the side effects that cannot be a value. Use it for the DOM, the persistence, the logging, and the external integration. Do not use it for the derived values, the data fetching, or the state updates. The effect is the escape hatch, and the escape hatch should be used rarely.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!