effect()
| Since: | Angular 16(2023) |
|---|
The effect() function in Angular registers a side effect that runs automatically whenever the Signals it reads change. Unlike computed(), which derives a new value, effect() is for operations with side effects — such as logging, DOM manipulation, or API calls — that should re-run whenever their Signal dependencies update. The function must be called within an injection context (component constructor, field initializer, or runInInjectionContext).
Syntax
import { Component, signal, effect } from '@angular/core';
@Component({ selector: 'app-example', template: `...`, standalone: true })
export class ExampleComponent {
count = signal(0);
constructor() {
effect(() => {
// Runs when count changes
console.log('count =', this.count());
});
}
}
effect() Options
| Option | Overview |
|---|---|
allowSignalWrites | By default, writing to a Signal inside an effect is not allowed and throws an error. Set { allowSignalWrites: true } to permit it. Use with caution to avoid infinite loops. |
injector | Specifies a custom Injector when calling effect() outside a natural injection context (e.g., in a factory function or test). |
manualCleanup | When true, the effect is not automatically cleaned up when the component is destroyed. You must call the returned EffectRef.destroy() manually. |
Sample Code
Logging and persisting a counter value to localStorage whenever it changes.
// counter.component.ts
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`,
standalone: true,
})
export class CounterComponent {
count = signal(0);
constructor() {
effect(() => {
const value = this.count();
console.log('count changed:', value);
localStorage.setItem('count', String(value));
});
}
increment(): void {
this.count.update(n => n + 1);
}
}
Monitoring multiple Signals at once and reacting when any of them change.
// search-filter.component.ts
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-search-filter',
template: `
<input [value]="keyword()" (input)="keyword.set($any($event.target).value)" />
<select [value]="category()" (change)="category.set($any($event.target).value)">
<option value="all">All</option>
<option value="books">Books</option>
<option value="music">Music</option>
</select>
<p>Searching "{{ keyword() }}" in "{{ category() }}"</p>
`,
standalone: true,
})
export class SearchFilterComponent {
keyword = signal('');
category = signal('all');
constructor() {
effect(() => {
// Runs whenever keyword or category changes
console.log('Search updated:', this.keyword(), this.category());
});
}
}
Subscribing to a WebSocket and cleaning up the connection with onCleanup.
// live-feed.component.ts
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-live-feed',
template: `<p>Latest: {{ latestMessage() }}</p>`,
standalone: true,
})
export class LiveFeedComponent {
roomId = signal('general');
latestMessage = signal('');
constructor() {
effect((onCleanup) => {
const id = this.roomId();
const ws = new WebSocket(`wss://example.com/rooms/${id}`);
ws.onmessage = (event) => {
this.latestMessage.set(event.data);
};
// Clean up the WebSocket when the effect re-runs or the component is destroyed
onCleanup(() => ws.close());
});
}
}
Summary
effect() connects Signal-based reactive state to the outside world. Every Signal read inside the callback becomes a tracked dependency; the callback re-runs automatically when any of them change. Cleanup logic can be registered via the onCleanup callback parameter, making it straightforward to handle resources like WebSockets, timers, or event listeners.
Avoid writing to Signals inside an effect unless you explicitly set { allowSignalWrites: true }, as this can lead to infinite update loops. For pure derived values, use computed() instead. For the Signal primitives, see computed().
Common Mistake: Writing to a Signal inside effect() without allowSignalWrites
By default, Angular prohibits writing to Signals inside an effect() to prevent infinite loops. Doing so throws a runtime error. If you genuinely need to update a Signal from an effect, pass { allowSignalWrites: true } — but first consider whether computed() is a better fit.
// NG: writing to a Signal inside effect without the option — runtime error
constructor() {
effect(() => {
this.doubled.set(this.count() * 2); // Error: not allowed
});
}
// OK option 1: use allowSignalWrites
constructor() {
effect(() => {
this.doubled.set(this.count() * 2);
}, { allowSignalWrites: true });
}
// OK option 2 (preferred): use computed() for derived values
doubled = computed(() => this.count() * 2);
Common Mistake: Calling effect() outside an injection context
effect() must be called within an injection context — the constructor, a field initializer, or a function called during initialization. Calling it in a lifecycle hook like ngOnInit or after construction causes an error.
// NG: called in ngOnInit — outside injection context
ngOnInit(): void {
effect(() => { console.log(this.count()); }); // Error
}
// OK: called in the constructor
constructor() {
effect(() => { console.log(this.count()); });
}
If you find any errors or copyright issues, please contact us.