ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 163
Numărul 162 Numărul 161 Numărul 160 Numărul 159 Numărul 158 Numărul 157 Numărul 156 Numărul 155 Numărul 154 Numărul 153 Numărul 152 Numărul 151 Numărul 150 Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 163
Abonamente

Fine-Grained Reactivity: Entuziasm, context tehnic și capcanele Signals în Angular

Edward Vlad
Software Engineer @ BMW TechWorks Romania



PROGRAMARE


Când modifici un singur câmp într-un formular complex, de ce frameworkul trebuie să verifice întreg component tree-ul pentru a detecta ce s-a schimbat? Pentru că așa a fost proiectat în mod implicit. Fie că vorbim de Reactivity system (Vue), Reconciliation (React) sau Change Detection (Angular), tehnologiile de front-end au încercat să răspundă la aceeași întrebare fundamentală: cum știm când state-ul s-a schimbat și ce parte din UI trebuie actualizată? Răspunsul tradițional a fost conservator - când nu ești sigur ce s-a schimbat, verifici tot. Asta funcționează, dar costă.

De ce este așa? De ce nu sunt performante implicit? Pentru că la momentul creării lor, alternative nu existau sau nu se rentau. Spre exemplu, React a ales Virtual DOM pentru simplitate declarativă, iar Angular a ales Zone.js pentru Automatic Change Detection. Ambele au privilegiat developer experience față de performanță - un tradeoff rezonabil în trecut, dar unul care devine problematic azi.

Soluția modernă poate fi signals. Așa cum AJAX a înlocuit full-page reloadul cu update-uri parțiale, fine-grained reactivity înlocuiește component-level re-rendering cu update-uri chirurgicale la nivel de nod DOM.

Ce sunt Signals?

Acest model este conceptual, menit să explice fundamentele mecanismului. Nu există (încă) o formă universal standardizată de signals în JavaScript. TC39, comitetul care decide ce intră în următoarele versiuni de JavaScript, are pe masa de lucru o propunere, dar deja fiecare framework (Angular, Solid, Vue etc.) implementează propriul mecanism cu diferențe semnificative de API și comportament.

În esență, signals sunt simple dar puternice. La nivel tehnic rudimentar, un signal folosește diverse soluții (cum ar fi closures) pentru a salva două lucruri: valoarea și lista de consumatori. Când un consumer (effect sau template) citește signalul, acesta din urmă îl înregistrează automat în lista de consumeri. Când valoarea se schimbă, signalul notifică toți consumerii din listă să se execute.

Cum am menționat și înainte, fiecare tehnologie de front-end a încercat să-și implementeze propria viziune și a avut o abordare proprie în ceea ce privește signals, menită să rezolve problemele de schimbare de state (și nu numai) la nivelul aplicațiilor web.

Ca să putem detalia puțin conceptul, am să mă refugiez în continuare în zona de Angular, mai explicit Angular Signals și să prezint câteva concepte introduse, care, deși sunt bazate pe modelul conceptual de mai sus, au dus lucrurile la un alt nivel.

Dependințele reactive în Angular

Începând cu Angular v16, frameworkul dezvoltat de Google a luat foarte în serios topicul Signals și, de la o versiune la alta, exercită un interes crescut în a dezvolta Angular Signals, în special datorită îmbunătățirilor de performanță. Frameworkul elimină dependența de Zone.js (bundle size mai mic), iar Change Detection devine mai eficient prin tracking granular. Împreună cu alte concepte introduse începând cu Angular v16, putem spune că frameworkul traversează o nouă etapă de maturizare (a treia, dacă ținem cont de AngularJS și Angular v2+).

Deși echipa Angular pare să introducă signals peste tot ca fiind o soluție perfectă pentru orice context, realitatea este mai nuanțată: aplicațiile pot avea în continuare probleme de arhitectură, reactive context leakul poate introduce buguri subtile, iar complexitatea unor operații asincrone necesită în continuare RxJS.

Ce e Reactive Context (ca să putem detalia cum se întâmplă aceste leakuri de reactive context, menționate mai sus)? Angular Signals folosesc o variabilă globală numită activeConsumer, care este setată automat de Angular când un consumer (template, computed signal sau effect) se execută.

Spre exemplu când un computed este citit de către template, Angular setează ca activeConsumer computedul respectiv - aici suntem în reactive contextul despre care vorbim, adică activeConsumer nu e null. Signalul sau signalurile din respectivul callback vor verifica dacă există ceva în activeConsumer, iar dacă nu e null, vor pune în lista lor de consumeri respectivul computed și astfel se creează legătura între consumer (computed) și producer (signal) despre care am povestit mai sus la nivel conceptual.

Tot acest sistem prin care Angular setează variabila globală activeConsumer atunci când un consumer se execută, poartă denumirea de Dynamic Dependency Tracking. Avantajul major este că nu avem nevoie să facem managementul dependințelor în cazul folosirii de signals, dar așa cum am menționat și mai sus, există și dezavantaje în a folosi Angular Signals sau mai bine spus aspecte la care trebuie atenție sporită.

Să presupunem că avem o componentă Angular care trebuie să seteze în localStorage o listă de numere și un index, de fiecare dată când lista din componentă se schimbă. Ambele, atât lista, cât și indexul sunt signals.

O astfel de componentă necesită un effect care să seteze acele date în localStorage, precum în implementarea de mai jos:

export class LocalStorageActionsPanel {
  someIndex: WritableSignal = signal(0);
  someDataIds: WritableSignal 
    = signal([1]);
  constructor() {
    effect((): void => {
      const dataIds: number[] = this.someDataIds();
      const index: number = this.someIndex();

      this.setDataToLocalStorage(dataIds, index);
    });

private setDataToLocalStorage(
    someDataIds: number[],
    someIndex: number
  ): void {
    // implementation here...
  }

  // some other related methods...
}

În implementarea de mai sus, orice schimbare a lui someIndex sau someDataIds va declanșa din nou efectul și va trimite datele în Local Storage.

De ce se întâmplă asta? Pentru că la prima execuție a effectului, Angular setează intern acel effect ca activeConsumer.

Când someIndex() sau someDataIds() au fost citite în interiorul effectului, fiecare signal a detectat că există un activeConsumer, adică effectul și l-a înregistrat în lista proprie de consumeri. Astfel, ambele signaluri au devenit dependențe reactive ale effectului. Iar la orice schimbare ulterioară a valorilor lor, ele notifică effectul să se execute.

În cazul lui someIndex s-a generat un reactive context leak, deoarece el nu trebuia să declanșeze executarea effectului. Acesta este un caz simplu și cu impact minor, iar soluția este să folosim funcția untracked(). Signalul din interiorul funcției untracked() nu va prelua activeConsumer în lista proprie de consumeri și deci, signalul nu va deveni dependință reactivă a effectului.

Deși pare că problema e rezolvată, metoda prin care trimitem datele în storage, poate la rândul ei să folosească signaluri sau să apeleze alte metode care o fac, iar asta implicit transformă citirea respectivelor signaluri în dependințe reactive - ceea ce ar însemna din nou reactive context leak.

effect((): void => {
  const dataIds: number[] = this.someDataIds();
  const index: number = 
    untracked((): number => this.someIndex());

  this.setDataToLocalStorage(dataIds, index);
});

Folosind-ne imaginația putem să intuim cât de multe probleme am putea genera dacă în loc de un index am avea 10, iar în loc de o metodă apelată, am avea 3, care, la rândul lor, ar folosi 3-4 signaluri de care nu știm. Toate acele signaluri sau chiar și computed (deoarece computed poate fi și consumer și producer) ar deveni dependințe reactive ale effectului și ar genera zeci poate chiar sute de execuții ale effectului, inutile, care ar scădea performanța aplicației, ar genera buguri foarte complexe, impredictibilitate logică (pentru că ar fi greu să știm când se execută effectul), chiar și memory leaks în cazuri extreme (aplicații foarte complexe).

Acesta este și motivul pentru care nu se recomandă utilizarea excesivă a funcțiilor effect în aplicație, poate fi foarte problematic dacă nu-l construim corect și nu ne asigurăm că nu generează reactive context leakuri.

Ca să ne asigurăm că effect se execută doar la schimbarea listei, vom pune și apelul metodei în untracked(), iar effectul va arăta astfel:

effect((): void => {
  const dataIds: number[] = this.someDataIds();

  untracked((): void => {
    const index: number = this.someIndex();
    this.setDataToLocalStorage(dataIds, index);
  });
});

Sumarizăm că: orice citire a unui signal într-un consumer devine dependință reactivă, atâta timp cât signalul nu este folosit în interiorul funcției untracked().

Concluzii

Fără îndoială Signals au reușit să îmbunătățească domeniul nostru de activitate și să ducă ideea și conceptul de fine-grained reactivity la următorul nivel, dar cum este și normal, vin cu o serie de precauții pe care noi trebuie să le studiem și să le avem în vedere când folosim signals.

Din punctul meu de vedere, la nivel de Angular, am trecut de la a folosi AsyncPipe, trackBy și diverse forme de unsubscribing pentru observables, la a analiza reactive contextul fiecărei parți din codul ce conține signals și la a mă asigura că nu arunc într-un haos logic toate schimbările venite odată cu Angular 16+. Rămâne de văzut ce ne rezervă viitorul cu privire la noua viziune signal-based a frameworkului

Surse:

  1. https://andamp.io/insights/blog/signals-in-javascript-a-soon-standard-or-overhyped

  2. https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob

  3. https://dev.to/ryansolid/building-a-reactive-library-from-scratch-1i0p

  4. https://medium.com/@eugeniyoz/angular-signals-reactive-context-and-dynamic-dependencytracking-d2d6100568b0

NUMĂRUL 163 - Trenduri în 2026

Sponsori

  • BT Code Crafters
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • GlobalLogic
  • BMW TechWorks Romania