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.
| Aspect | Effect | Subscription |
|---|---|---|
| Source | Signals | Observables |
| Timing | After change detection, microtask | Synchronous on emission |
| Dependencies | Automatic | The observable |
| Cleanup | onCleanup | unsubscribe or takeUntil |
| Lifetime | Injection context | Manual |
| Values | The current signal values | The emitted values |
| Use | Side effects on signal change | Stream 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
| Form | Purpose |
|---|---|
effect(() => { ... }) | Basic side effect |
effect((onCleanup) => { ... }) | With cleanup |
The Effect’s Timing
| Timing | Value |
|---|---|
| Immediate run | At creation |
| Re-run | On dependency change |
| Schedule | Microtask, after change detection |
| Coalescing | One run per synchronous block |
The Cleanup
| Pattern | Purpose |
|---|---|
onCleanup(fn) | Register a cleanup |
| Multiple calls | Multiple cleanups |
| On re-run | Runs before the next run |
| On destroy | Runs when the context is destroyed |
When to Use an Effect
| Use | Example |
|---|---|
| DOM update | document.body.classList.toggle(...) |
| Persistence | localStorage.setItem(...) |
| Logging | analytics.track(...) |
| External integration | chart.update(...) |
| Initialization | service.register(...) |
When Not to Use an Effect
| Do not use for | Use instead |
|---|---|
| Derived value | computed |
| Data fetching | switchMap or resource |
| State update | set or update |
| Stream processing | RxJS operators |
| Template value | Signal read in the template |
The Effect vs the Subscription
| Aspect | Effect | Subscription |
|---|---|---|
| Source | Signals | Observables |
| Timing | After change detection | On emission |
| Cleanup | onCleanup | unsubscribe |
| Lifetime | Injection context | Manual |
| Use | Side effects on signal change | Stream 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
| Pitfall | Problem | Solution |
|---|---|---|
| Effect writes to its source | Infinite loop | Use computed or a different signal |
| Effect fetches data | Redundant requests | Use switchMap |
| Effect computes a value | Wrong tool | Use computed |
| Missing cleanup | Resource leak | Use onCleanup |
| Effect outside the injection context | Compile error | Create in the constructor |
| Effect used as the default | Fights the model | Use computed |
| Synchronous expectation | Deferred run | Write for the timing |
| Conditional dependency missed | Surprise | Know 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
| Aspect | Value |
|---|---|
| Created with | effect(fn) |
| Runs | Immediately, then on dependency change |
| Timing | Microtask, after change detection |
| Cleanup | onCleanup(fn) |
| Lifetime | Injection context |
| Tracks | The signals read |
| Produces | No value (side effect only) |
| Use an effect for | Use something else for |
|---|---|
| DOM updates | Derived value → computed |
| Persistence | State update → set/update |
| Logging | Data fetch → switchMap/resource |
| External integration | Stream processing → RxJS |
| Initialization | Template value → signal read |
Key takeaways:
- An
effectruns 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
onCleanupcallback 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
computedis the tool, and the effect that computes is a mistake - The effect should not fetch data — the
switchMapof the signal’s stream or theresourceAPI 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
computedor 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
computedfirst
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!