Pe pagina signal() și computed() ai văzut ce fac semnalele. Pagina asta arată cum funcționează pe dinăuntru: un graf reactiv construit automat din citiri, în care scrierile împing doar o notificare, iar recalcularea are loc abia când cineva citește. De aici vin toate proprietățile utile: computed e leneș și memorat, nu vezi niciodată stări intermediare inconsistente, iar Angular poate re-randa doar componentele afectate, fără Zone.js.
computed scump nu costă nimic dacă nu e citit, de ce items().push() nu actualizează UI-ul, de ce un effect care setează alt semnal e o idee proastă, sau de ce un semnal citit într-o ramură if uneori „nu reacționează”.Producători, consumatori și graful
Fiecare semnal e un nod în graf. signal() e un producător. computed() e și consumator (citește alte semnale), și producător (alții îl citesc pe el). effect() și template-urile componentelor sunt consumatori. În timpul rulării unei funcții reactive există un consumator activ, iar orice semnal citit în acel moment devine dependența lui. Graful nu se declară: se construiește din citiri.
Fiecare nod are o version care crește doar când produce o valoare diferită de cea anterioară. Unii consumatori sunt „live” (effect-urile și template-urile); pentru ei, graful ține și muchii inverse, de la producător la consumator, ca să le poată trimite notificări. Tipurile primitive din @angular/core/primitives/signals arată exact aceste câmpuri: version, dirty, producers, consumers.
const a = signal(1); // producător
const b = signal(2); // producător
const sum = computed(() => a() + b()); // consumator ȘI producător
const product = computed(() => a() * b());
const result = computed(() => sum() + product()); // diamant: a și b ajung aici pe două drumuri
// În timpul rulării unei funcții reactive, Angular ține un „consumator activ”.
// Orice semnal citit acum devine dependența lui. Nu declari nimic: graful se construiește din citiri.Push, apoi pull
O scriere (set/update) nu recalculează nimic. Doar crește versiunea semnalului și anunță consumatorii live că ar putea fi murdari: aceasta e faza push. Când consumatorul rulează din nou (template-ul la următoarea randare, effect-ul la rândul lui), citește ce are nevoie, iar fiecare computed își verifică producătorii: s-a schimbat vreo versiune? Doar atunci se recalculează. Aceasta e faza pull.
a.set(5)
│ PUSH: a își crește `version` și anunță consumatorii „live” (template, effect) că POT fi murdari.
│ Nimic nu se recalculează încă.
▼
template / effect marcat dirty → change detection programează o randare
│
▼
template citește result()
│ PULL: result își verifică producătorii (sum, product) — au altă versiune?
│ sum își verifică producătorii (a, b) — a are altă versiune → sum se recalculează.
│ Dacă valoarea nouă e EGALĂ cu cea veche, versiunea lui sum nu crește → propagarea se oprește.
▼
result se recalculează o singură dată, cu valori deja consistente (glitch-free)Consecința importantă: un computed se recalculează abia după ce toate scrierile s-au terminat, cu valori deja consistente. În diamantul din demo, result nu vede niciodată sum nou cu product vechi. Asta se numește glitch-free. Iar dacă modifici a și b în același click, result se recalculează o singură dată.
În template: a = 1, b = 2 · result = 5
Lene și memorare
Documentația Angular o spune direct: un computed e evaluat leneș și memorat. Funcția nu rulează până la prima citire. Apoi valoarea e păstrată și refolosită până când o dependență se schimbă. Practic, poți pune derivări scumpe (filtrare, sortare) în computed: costă doar când chiar e nevoie de rezultat.
const expensive = computed(() => {
console.log('calculez…');
return items().filter(matches).sort(byPrice);
});
items.set(newItems); // nu se afișează nimic: nimeni n-a citit încă
expensive(); // „calculez…” — prima citire
expensive(); // nimic: valoare memorată (memoized)
items.set(other); // tot nimic — marcat ca vechi, nu recalculat
expensive(); // „calculez…”Egalitate: unde se oprește propagarea
Versiunea unui nod crește doar dacă valoarea nouă nu e „egală” cu cea veche. Implicit egalitatea e Object.is: pentru obiecte și array-uri contează referința, nu conținutul. Același mecanism se aplică și unui computed. Dacă rezultatul lui nu se schimbă, consumatorii lui nu se mai recalculează, chiar dacă dependențele lui s-au schimbat. Cu { equal } definești tu ce înseamnă „la fel”.
// Implicit: Object.is. Pentru obiecte/array-uri contează REFERINȚA.
const tags = signal(['a', 'b']);
tags.set(['a', 'b']); // altă referință → valoare „nouă” → consumatorii se recalculează
// Egalitate custom: valoarea nu se schimbă dacă e „la fel” după regula ta
const sameItems = (x: string[], y: string[]) => x.length === y.length && x.every((v, i) => v === y[i]);
const stableTags = signal(['a', 'b'], { equal: sameItems });
stableTags.set(['a', 'b']); // egal → versiunea nu crește → nimic downstream
// Și un computed are egalitate: dacă rezultatul e egal, propagarea se oprește la el
const parity = computed(() => (n() % 2 ? 'impar' : 'par')); // n: 0 → 2 → 4 … parity rămâne 'par'| Nod | Valoare | Recalculări |
|---|---|---|
parity = computed(() => n() % 2 ? 'impar' : 'par') | ||
label = computed(() => 'Numărul e ' + parity()) |
| Semnal | count = computed(tags().length) |
|---|---|
signal(...) — egalitate implicită (Object.is) | 2 · recalculat × |
signal(..., { equal: sameItems }) | 2 · recalculat × |
Dependențe dinamice
La fiecare rulare, lista de producători a unui consumator se reconstruiește din ce a citit efectiv. Un semnal citit doar într-o ramură if e dependență doar cât timp acea ramură se execută. E un câștig de performanță, dar și o sursă de confuzie: „schimb details și nu se întâmplă nimic” e de obicei corect, pentru că ramura curentă nu-l citește.
const view = computed(() => {
if (showDetails()) {
return details(); // dependențe: showDetails + details
}
return summary(); // dependențe: showDetails + summary
});
// La fiecare rulare, lista de producători se reconstruiește din ce s-a citit EFECTIV.
// Cât timp showDetails() e false, schimbarea lui details nu recalculează view.view() = rezumat v0
Dependențe înregistrate la ultima rulare: · view recalculat ×
Effect-uri și template-uri: consumatorii live
Effect-urile rulează asincron, în timpul procesului de change detection, nu în momentul set(). Documentația v22 distinge două feluri. Un view effect e creat în contextul unei componente: rulează înainte ca acea componentă să fie verificată și e distrus odată cu ea. Un root effect e creat într-un serviciu din root: rulează înainte de verificarea tuturor componentelor și trăiește cât aplicația. Dacă o dependență se schimbă chiar în timpul execuției, effect-ul mai rulează o dată înainte să continue change detection.
// View effect: creat în contextul unei componente → rulează ÎNAINTE ca acea componentă
// să fie verificată de change detection; e distrus odată cu componenta.
@Component({ /* ... */ })
export class Profile {
constructor() {
effect(() => console.log('profil', this.user()));
}
}
// Root effect: creat într-un serviciu din root → rulează înaintea verificării tuturor componentelor;
// trăiește cât aplicația.
@Injectable({ providedIn: 'root' })
export class Analytics {
constructor() {
effect(() => track(this.route()));
}
}
// Ambele sunt asincrone (nu rulează în momentul set()), legate de procesul de change detection.Template-ul unei componente e și el un consumator reactiv. Când un semnal citit în template se schimbă, Angular marchează componenta pentru verificare (ca markForCheck), iar change detection o re-randează doar pe ea și strămoșii ei. Pe asta se sprijină aplicațiile zoneless și OnPush implicit din v22 (vezi Change detection).
@Component({
template: `<p>{{ fullName() }}</p>`, // template-ul e un consumator reactiv
})
export class Card {
readonly first = signal('Ana');
readonly fullName = computed(() => `${this.first()} Pop`);
}
// first.set('Ioana') → nodul reactiv al template-ului e marcat dirty → componenta e marcată pentru
// verificare (ca markForCheck) → change detection o re-randează. Fără Zone.js, fără verificarea
// întregului arbore: așa funcționează aplicațiile zoneless.Când DA, când NU
- Derivări scumpe în
computed: se calculează doar la nevoie și o singură dată per schimbare. - Lanțuri de computed-uri în loc de un singur computed uriaș. Cu equality cutoff, schimbările mici se opresc devreme.
{ equal }pentru valori recreate des, dar „la fel” (filtre, liste de id-uri de la server).untrackedpentru citiri care nu trebuie să declanșeze nimic (logging, analytics).
- Nu detectează mutații:
pushpe un array dintr-un semnal nu anunță pe nimeni. - Nu îți ordonează efectele secundare: dacă ai nevoie de ordine strictă în timp, folosește RxJS (vezi RxJS avansat).
- Nu e un mecanism de evenimente: „s-a dat click” nu e stare. Folosește
output()sau un Subject. - Nu rulează effect-urile sincron: dacă ai nevoie de rezultat imediat, derivă cu
computed.
Capcane frecvente
// ✗ Scriere într-un computed — eroare: „Writing to signals is not allowed in a `computed`”
const total = computed(() => {
count.set(items().length); // efect secundar în derivare
return items().reduce(sum, 0);
});
// ✗ Effect folosit pentru derivare (o randare în plus, risc de bucle)
effect(() => total.set(items().reduce(sum, 0)));
// ✓ Derivarea e un computed
const total2 = computed(() => items().reduce(sum, 0));
// ✗ Mutare: aceeași referință → Object.is spune „egal” → nimic nu se actualizează
items().push(newItem);
// ✓
items.update((list) => [...list, newItem]);- „Nu reacționează”: verifică dacă semnalul e citit pe ramura care rulează și dacă îl citești cu
(). O referință la semnal fără apel nu creează dependență. - Obiecte mari într-un singur semnal: orice câmp schimbat recalculează tot ce depinde de obiect. Împarte starea sau derivă câmpurile cu
computed. - Effect care scrie în alte semnale: randări în plus și risc de bucle. Aproape mereu e un
computedsau un linkedSignal. - Citiri în constructor „ca să fie gata”: un computed citit prea devreme se calculează cu date incomplete. Lasă-l leneș.
Fișă rapidă
NOD ROL CÂND RULEAZĂ
signal producător — (doar își schimbă valoarea și versiunea la set/update)
computed consumator + producător lazy: la citire, doar dacă vreo dependență s-a schimbat
effect consumator „live” asincron, în change detection (view: înaintea componentei; root: înaintea tuturor)
template consumator „live” când componenta e re-randată după ce a fost marcată dirty
REGULI
dependențele = ce citești EFECTIV la ultima rulare (dinamice, pe ramuri)
egalitate implicită Object.is; { equal } pentru valori „la fel” → oprește propagarea
computed fără cititori nu se calculează deloc
nu scrie semnale în computed; nu deriva stare cu effect
untracked(() => …) citește fără să creeze dependență