effect() rulează o funcție o dată, apoi din nou de fiecare dată când se schimbă un semnal pe care l-a citit. E puntea dintre lumea reactivă (semnale) și lumea care nu știe de semnale: DOM manual, localStorage, librării terțe, analytics, console.
Un effect duce date din semnale spre exterior. Dacă îl folosești ca să setezi alt semnal, aproape sigur îți trebuia un computed() sau un linkedSignal().
Sintaxa
Creezi efectul într-un context de injecție (constructor sau inițializare de câmp). Efectul e legat automat de ciclul de viață al componentei sau serviciului: când acesta e distrus, și efectul e oprit.
import { Component, effect, signal } from '@angular/core';
@Component({ /* ... */ })
export class Settings {
readonly theme = signal<'light' | 'dark'>('light');
constructor() {
// Rulează o dată la creare, apoi de fiecare dată când `theme` se schimbă.
effect(() => {
document.documentElement.dataset['theme'] = this.theme();
localStorage.setItem('theme', this.theme());
});
}
}Când rulează exact?
- Nu sincron: dacă setezi un semnal de 3 ori la rând, efectul rulează o singură dată, cu valoarea finală.
- Efectele de componentă rulează în timpul change detection, înainte ca template-ul componentei să fie verificat.
- Efectele din servicii root rulează ca microtask-uri, înainte de change detection.
- Dependențele sunt dinamice: contează doar semnalele citite la ultima rulare.
Cleanup: onCleanup
Efectul primește o funcție onCleanup. Callback-ul înregistrat rulează înainte de următoarea execuție și la distrugere. E locul unde oprești timere, dezabonezi listener-e, anulezi request-uri.
effect((onCleanup) => {
if (!this.running()) return;
const id = setInterval(() => this.ticks.update((t) => t + 1), this.speed());
// Se apelează înainte de următoarea rulare a efectului
// și când componenta / serviciul e distrus.
onCleanup(() => clearInterval(id));
});- Apasă „Pornește”. Schimbă intervalul în timp ce rulează.
afterNextRender & afterRenderEffect: când îți trebuie DOM-ul
Dacă trebuie să măsori sau să modifici DOM-ul după ce Angular l-a actualizat, folosește hook-urile de randare. afterNextRender rulează o singură dată (perfect pentru a inițializa o librărie de grafice sau hărți), iar afterRenderEffect e un effect care rulează după randare, doar când semnalele lui s-au schimbat. Ambele rulează doar în browser (nu pe server la SSR).
import { afterNextRender, afterRenderEffect, ElementRef, viewChild } from '@angular/core';
export class AutoGrowTextarea {
private readonly area = viewChild.required<ElementRef<HTMLTextAreaElement>>('area');
readonly text = signal('');
constructor() {
// Rulează DUPĂ ce Angular a actualizat DOM-ul, ori de câte ori `text` se schimbă.
afterRenderEffect(() => {
this.text();
const el = this.area().nativeElement;
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
});
// O singură dată, după prima randare (ideal pentru librării care au nevoie de DOM).
afterNextRender(() => {
this.chart = new ThirdPartyChart(this.canvas().nativeElement);
});
}
}
// Cu faze explicite — eviți „layout thrashing” când ai multe componente:
afterRenderEffect({
earlyRead: () => this.host.nativeElement.getBoundingClientRect().width, // citește DOM
write: (width) => { this.panel().nativeElement.style.width = `${width()}px`; }, // scrie DOM
});Anti-pattern-uri (și ce folosești în loc)
1. Sincronizarea între semnale
// ✓ computed pentru stare derivată
readonly fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
// ✓ linkedSignal dacă valoarea derivată trebuie să fie și editabilă
readonly selected = linkedSignal(() => this.options()[0]);// ✗ effect folosit ca să „sincronizeze” stare
readonly firstName = signal('Ana');
readonly lastName = signal('Pop');
readonly fullName = signal('');
constructor() {
effect(() => this.fullName.set(`${this.firstName()} ${this.lastName()}`));
}
// Probleme: o rulare în plus, un moment în care fullName e vechi,
// și un graf de dependențe greu de urmărit.firstName.set(…)computed()Ana Popeffect() + set()Ana Poprulări ale efectului: 1Pe ecran cele două arată la fel: efectul unei componente rulează la change detection, înainte ca template-ul ei să fie actualizat. Diferența apare în codul care citește valoarea imediat după set().
Citit în același handler de click, imediat după set()
- Apasă un nume: vezi ce întoarce fiecare variantă chiar în acel moment.
2. Fetch de date din effect
// ✓ resource / httpResource: anulare automată, status, error, reload
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);// ✗ HTTP declanșat dintr-un effect
effect(() => {
const id = this.userId();
this.http.get<User>(`/api/users/${id}`).subscribe((u) => this.user.set(u));
// fără anulare: răspunsurile vechi pot sosi după cele noi (race condition)
});Contextul de injecție
effect() are nevoie de un injector ca să știe când să se distrugă. În afara constructorului primești eroarea NG0203. Soluția: creezi efectul ca field sau în constructor; în cazuri rare îi dai explicit injector.
export class Dashboard {
private readonly injector = inject(Injector);
// ✓ în field initializer sau constructor: context de injecție disponibil
private readonly logEffect = effect(() => console.log(this.filter()));
ngOnInit() {
// ✗ aici nu mai ești în context de injecție → eroare NG0203
// effect(() => ...);
// ✓ dacă chiar trebuie mai târziu, dă-i explicit injectorul
effect(() => console.log(this.filter()), { injector: this.injector });
}
stop() {
this.logEffect.destroy(); // oprire manuală (rar nevoie — se distruge singur cu componenta)
}
}Înainte să scrii un effect, verifică dacă nu e de fapt stare derivată (signal() și computed()) sau stare care se resetează (linkedSignal() — stare derivată editabilă). De ce rulează un effect exact când rulează afli la Semnalele pe dinăuntru.
Când DA, când NU
- Persistare:
localStorage,sessionStorage, IndexedDB. - Logging, analytics, telemetrie.
- Librării imperative: grafice (Chart.js), hărți (Leaflet), editoare, canvas.
- API-uri de browser:
document.title, focus, scroll, clase pe<html>. - Sincronizare cu cod non-Angular (un store vechi, un widget jQuery).
- Stare derivată →
computed(). - Stare derivată editabilă / resetabilă →
linkedSignal(). - Request-uri la schimbarea unui parametru →
resource()/httpResource(). - Reacție la un click → pune logica direct în handler-ul evenimentului.
- Validări de formular → Signal Forms au propriile reguli.
Capcane frecvente
- Bucle infinite: un efect care citește și scrie același semnal se poate re-declanșa la nesfârșit. Folosește
untracked()pentru citirea valorii curente sau, mai bine, regândește fluxul. - Dependențe ascunse: dacă apelezi o metodă care citește semnale în interior, și ele devin dependențe. Împachetează apelul în
untracked(() => ...). - Așteptarea rulării sincrone: după
signal.set()efectul NU a rulat încă. Nu scrie cod care depinde de asta. - effect în ngOnInit: eroare de context de injecție. Mută-l în constructor.
Fișă rapidă
effect(() => { /* citește semnale, face efecte secundare */ });
effect((onCleanup) => { const id = setInterval(...); onCleanup(() => clearInterval(id)); });
effect(fn, { injector, manualCleanup: true, debugName: 'sync-storage' });
const ref = effect(fn); ref.destroy();
afterNextRender(() => { /* o dată, după prima randare — init librării DOM */ });
afterRenderEffect(() => { /* după fiecare randare în care semnalele citite s-au schimbat */ });
afterRenderEffect({ earlyRead, write, mixedReadWrite, read });
// Regula: effect = „lumea reactivă → lumea non-reactivă”. Niciodată semnal → semnal.