📚 Introducere
Angular oferă două paradigme puternice pentru gestionarea stării și a datelor reactive: Observables (RxJS) și Signals (Angular 16+). Înțelegerea diferențelor și a cazurilor de utilizare este crucială pentru orice dezvoltator Angular modern.
🔄 Observables (RxJS)
Ce sunt Observables?
Observables sunt stream-uri de date asincrone care pot emite valori multiple în timp. Sunt lazy (se execută doar când cineva se abonează) și oferă operatori puternici pentru transformarea datelor.
// Exemplu de bază Observable
import { Observable, of, interval } from 'rxjs';
import { map, filter, debounceTime, take } from 'rxjs/operators';
// Observable simplu
const myObservable = new Observable<string>(observer => {
observer.next('Prima valoare');
observer.next('A doua valoare');
setTimeout(() => {
observer.next('Valoare întârziată');
observer.complete();
}, 1000);
});
// Subscribing
myObservable.subscribe({
next: (value: string) => console.log(value),
complete: () => console.log('Completat!')
});
// Observable cu operatori
const numbers$ = interval(1000).pipe(
filter(n => n % 2 === 0),
map(n => n * 2),
take(5)
);
// Emite: 0, 4, 8, 12, 16
Caracteristici cheie
🚀 Lazy Evaluation
Se execută doar când există un subscriber
♻️ Multicast
Pot avea multiple subscripții
🎯 Operatori
100+ operatori pentru transformări complexe
⏱️ Asincron
Perfect pentru operații asincrone
Avantaje
- Operatori puternici (map, filter, switchMap)
- Anulare ușoară (unsubscribe)
- Compoziție complexă de stream-uri
- Gestionare erori robustă
- Backpressure handling
Dezavantaje
- Curbă de învățare abruptă
- Necesită unsubscribe manual
- Verbose pentru cazuri simple
- Memory leaks dacă nu se gestionează corect
- Complexitate pentru începători
🛠️ Operatori RxJS
Operatorii RxJS sunt funcții pure care permit transformarea și manipularea stream-urilor de date. Sunt cheia puterii Observables și se împart în mai multe categorii.
📥 Creation Operators
Creează observables din diverse surse de date.
import {
of, from, interval, timer, fromEvent,
range, empty, NEVER, throwError
} from 'rxjs';
// of - creează din valori statice
const values$ = of(1, 2, 3, 4, 5);
// from - creează din array, Promise, iterable
const fromArray$ = from([1, 2, 3, 4, 5]);
const fromPromise$ = from(fetch('/api/data'));
// interval - emite valori la interval regulat
const interval$ = interval(1000); // 0, 1, 2, 3...
// timer - întârzie prima emisie, apoi interval
const timer$ = timer(2000, 1000); // după 2s: 0, 1, 2...
// fromEvent - din DOM events
const clicks$ = fromEvent(button, 'click');
// range - secvență numerică
const range$ = range(1, 10); // 1, 2, 3... 10
map
Transformă valorile 1:1
users$.pipe(map(u => u.name))filter
Filtrează pe condiții
nums$.pipe(filter(n => n > 0))switchMap
Switch la nou observable
search$.pipe(switchMap(callAPI))debounceTime
Întârzie până se liniștește
input$.pipe(debounceTime(300))combineLatest
Combină ultimele valori
combineLatest([a$, b$])catchError
Gestionează erorile
http$.pipe(catchError(handleErr))⚡ Signals (Angular 16+)
Ce sunt Signals?
Signals sunt o nouă primitivă reactivă în Angular care oferă o modalitate simplă și performantă de a gestiona starea. Sunt sincrone, au tracking automat al dependențelor și nu necesită unsubscribe.
import { signal, computed, effect } from '@angular/core';
// Signal simplu
const count = signal(0);
// Citire valoare
console.log(count()); // 0
// Actualizare valoare
count.set(5);
count.update(value => value + 1);
// Computed signal
const doubleCount = computed(() => count() * 2);
// Effect - se execută automat când se schimbă dependențele
effect(() => {
console.log(`Count este: ${count()}`);
});
// În component
@Component({
template: `
<div>Count: {{ count() }}</div>
<div>Double: {{ doubleCount() }}</div>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
increment(): void {
this.count.update(v => v + 1);
}
}
Caracteristici cheie
🎯 Simplicitate
API simplu și intuitiv
🔄 Reactiv
Tracking automat al dependențelor
⚡ Performanță
Fine-grained reactivity
🧹 No Cleanup
Nu necesită unsubscribe
⚖️ Comparație Detaliată
| Aspect | Observables | Signals |
|---|---|---|
| Natura | Asincron | Sincron |
| Valori | Stream de valori multiple | O singură valoare curentă |
| Lazy/Eager | Lazy (necesită subscribe) | Eager (valoare imediată) |
| Cleanup | Necesită unsubscribe | Automat |
| Operatori | 100+ operatori RxJS | computed(), effect() |
| Use Case | HTTP, WebSocket, Events | State management, UI updates |
| Performanță | Bună pentru stream-uri | Excelentă pentru UI updates |
| Learning Curve | Abruptă | Ușoară |
Când să folosești ce?
Folosește Observables pentru:
- 🌐 HTTP requests și comunicare API
- 🔌 WebSocket connections
- ⏱️ Timers și intervale
- 🎮 Event streams complexe
- 🔄 Operații care necesită retry/cancel
- 🎯 Transformări complexe de date
Folosește Signals pentru:
- 📦 State management local
- 🎨 UI state (loading, errors)
- 📊 Computed values simple
- ⚡ Performanță optimă în templates
- 🔢 Contoare și toggle-uri
- 📝 Form state simplu
💻 Exemple Practice
Exemplu 1: Shopping Cart cu Signals
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
// State cu Signals
private itemsSignal = signal<CartItem[]>([]);
// Public API
items = this.itemsSignal.asReadonly();
// Computed values
totalItems = computed(() =>
this.itemsSignal().reduce((sum, item) => sum + item.quantity, 0)
);
totalPrice = computed(() =>
this.itemsSignal().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
);
isEmpty = computed(() => this.itemsSignal().length === 0);
// Methods
addItem(product: Product): void {
this.itemsSignal.update(items => {
const existing = items.find(i => i.id === product.id);
if (existing) {
return items.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...product, quantity: 1 }];
});
}
}
Exemplu 2: Search cu Observables
@Component({
template: `
<input [formControl]="searchControl" placeholder="Search...">
<div *ngFor="let user of users$ | async">{{ user.name }}</div>
`
})
export class SearchComponent implements OnInit {
searchControl = new FormControl('');
users$ = new BehaviorSubject<User[]>([]);
ngOnInit(): void {
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term !== null && term.length > 2),
switchMap(term => this.searchUsers(term!)),
takeUntilDestroyed()
).subscribe(users => this.users$.next(users));
}
private searchUsers(term: string): Observable<User[]> {
return this.http.get<User[]>(`/api/users/search?q=${term}`);
}
}
🏋️ Exerciții Practice
Exercițiu 1: Todo List cu Signals
Creează un serviciu TodoService folosind Signals care să implementeze:
- Adăugare/ștergere todo-uri
- Marcare ca completat
- Filtrare (all, active, completed)
- Counter pentru todo-uri active
@Injectable({ providedIn: 'root' })
export class TodoService {
private todosSignal = signal<Todo[]>([]);
private filterSignal = signal<'all' | 'active' | 'completed'>('all');
todos = this.todosSignal.asReadonly();
filter = this.filterSignal.asReadonly();
filteredTodos = computed(() => {
const todos = this.todosSignal();
const filter = this.filterSignal();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
});
activeTodosCount = computed(() =>
this.todosSignal().filter(t => !t.completed).length
);
addTodo(title: string): void {
if (!title.trim()) return;
const newTodo: Todo = {
id: crypto.randomUUID(),
title: title.trim(),
completed: false,
createdAt: new Date()
};
this.todosSignal.update(todos => [newTodo, ...todos]);
}
toggleTodo(id: string): void {
this.todosSignal.update(todos =>
todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
}
}
🎯 Întrebări Frecvente la Interviu
1. Care este diferența principală între Observables și Signals?
Răspuns:
- Observables sunt stream-uri asincrone care emit valori multiple în timp, sunt lazy (necesită subscribe), și oferă operatori complecși pentru transformări.
- Signals sunt valori reactive sincrone care stochează o singură valoare curentă, sunt eager (citire imediată), și oferă tracking automat al dependențelor.
- Observables sunt ideale pentru operații asincrone (HTTP, WebSocket), iar Signals pentru state management local și UI updates.
2. Cum previi memory leaks cu Observables?
Răspuns - 5 metode principale:
// 1. takeUntilDestroyed() - Angular 16+
data$ = this.service.getData().pipe(
takeUntilDestroyed()
);
// 2. DestroyRef inject
constructor(private destroyRef: DestroyRef) {
this.service.getData().pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe();
}
// 3. Async pipe în template
// Template: {{ data$ | async }}
// 4. Manual unsubscribe
private subscription: Subscription;
ngOnInit() {
this.subscription = this.service.getData().subscribe();
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
3. Cum convertești între Observables și Signals?
// Observable to Signal
import { toSignal } from '@angular/core/rxjs-interop';
const data$ = this.http.get('/api/data');
const dataSignal = toSignal(data$, { initialValue: [] });
// Signal to Observable
import { toObservable } from '@angular/core/rxjs-interop';
const countSignal = signal(0);
const count$ = toObservable(countSignal);
4. Explică computed() și effect() în Signals
Răspuns:
- computed() - Creează un signal derivat care se recalculează automat când dependențele se schimbă. Este lazy, memorized și read-only.
- effect() - Execută side-effects când dependențele se schimbă. Rulează în contexte de injecție și se cleanup automat.
const price = signal(100);
const quantity = signal(2);
// Computed - derivă valoare nouă
const total = computed(() => price() * quantity());
// Effect - side effects
effect(() => {
console.log(`Total changed to: ${total()}`);
localStorage.setItem('total', total().toString());
});
5. Care sunt best practices pentru Signals în Angular?
Best Practices:
- Folosește
asReadonly()pentru a expune signals publice - Preferă
update()în loc deset()pentru transformări - Folosește
computed()pentru valori derivate în loc de effect() - Evită mutații directe ale obiectelor în signals
- Grupează signals related într-un serviciu
6. Explică diferențele între switchMap, mergeMap, concatMap și exhaustMap
// switchMap - anulează observable-ul anterior searchTerm$.pipe( switchMap(term => searchService.search(term)) ); // Perfect pentru search - anulează request-ul anterior // mergeMap - execută toate în paralel ids$.pipe( mergeMap(id => fetchUser(id)) ); // Pentru operații independente, fără ordine // concatMap - execută în ordine, unul după altul requests$.pipe( concatMap(req => processRequest(req)) ); // Pentru operații care trebuie să respecte ordinea // exhaustMap - ignoră până se completează cel curent saveButton$.pipe( exhaustMap(() => saveData()) ); // Previne multiple submit-uri
7. Cum gestionezi error handling în RxJS?
// catchError - prinde și gestionează erorile
this.http.get('/api/data').pipe(
catchError((error: HttpErrorResponse) => {
console.error('Error:', error);
// Returnează valoare default
return of({ data: [], error: true });
// SAU re-aruncă eroarea
// return throwError(() => new Error('Failed'));
})
);
// retry - încearcă din nou de n ori
this.http.get('/api/data').pipe(
retry(3),
catchError(error => of({ error: true }))
);
// retryWhen - retry cu logică customizată
this.http.get('/api/data').pipe(
retryWhen(errors => errors.pipe(
tap(error => console.log('Retrying:', error)),
delay(1000),
take(3)
))
);
// finalize - execută cod la sfârșit (success/error)
this.http.get('/api/data').pipe(
finalize(() => {
this.loading = false;
})
);
8. Ce este Cold vs Hot Observable?
Cold Observable:
- Creează un producer nou pentru fiecare subscriber
- Fiecare subscription primește toate valorile de la început
- Exemple: HTTP requests, timer(), of()
Hot Observable:
- Partajează același producer între toți subscribers
- Subscribers primesc doar valorile emise după subscription
- Exemple: DOM events, Subject
// Cold - fiecare subscriber primește propria execuție
const cold$ = new Observable(observer => {
console.log('Producer started');
observer.next(Math.random());
});
cold$.subscribe(v => console.log('Sub1:', v));
cold$.subscribe(v => console.log('Sub2:', v));
// Output: Două valori diferite
// Hot - cu share() sau Subject
const hot$ = cold$.pipe(share());
const subject$ = new Subject();
// Convert Cold to Hot
const shared$ = this.http.get('/api/data').pipe(
shareReplay(1)
);
9. Cum implementezi caching cu Observables?
@Injectable()
export class DataService {
private cache = new Map<string, Observable<any>>();
// Method 1: shareReplay pentru cache simplu
getData(): Observable<Data[]> {
if (!this.cache.has('data')) {
const data$ = this.http.get<Data[]>('/api/data').pipe(
shareReplay(1)
);
this.cache.set('data', data$);
}
return this.cache.get('data')!;
}
// Method 2: Cache cu expirare
getDataWithTTL(ttl: number = 5000): Observable<Data[]> {
const cacheKey = 'data-ttl';
const cached = this.cache.get(cacheKey);
if (cached) {
return cached;
}
const data$ = this.http.get<Data[]>('/api/data').pipe(
shareReplay(1),
finalize(() => {
// Clear cache după TTL
setTimeout(() => {
this.cache.delete(cacheKey);
}, ttl);
})
);
this.cache.set(cacheKey, data$);
return data$;
}
// Method 3: Cache cu invalidare manuală
clearCache(key?: string): void {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
}
10. Cum testezi Observables și Signals?
// Testing Observables cu marble testing
describe('Observable Tests', () => {
let scheduler: TestScheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should debounce search', () => {
scheduler.run(({ hot, cold, expectObservable }) => {
const input$ = hot('a-b-c---|');
const expected = ' --c---|';
const result$ = input$.pipe(debounceTime(20));
expectObservable(result$).toBe(expected);
});
});
});
// Testing Signals
describe('Signal Tests', () => {
it('should update computed when signal changes', () => {
const count = signal(0);
const doubled = computed(() => count() * 2);
expect(doubled()).toBe(0);
count.set(5);
expect(doubled()).toBe(10);
});
it('should track effect execution', () => {
const count = signal(0);
let effectRuns = 0;
effect(() => {
count(); // trigger dependency
effectRuns++;
});
expect(effectRuns).toBe(1);
count.set(1);
expect(effectRuns).toBe(2);
});
});
📚 Resurse Utile
📖 Documentație Oficială
Angular Signals Guide și RxJS Documentation pentru referințe complete
🎥 Video Tutorials
Angular University și Decoded Frontend pentru explicații detaliate
🛠️ Tools & Libraries
RxJS DevTools, Marble Testing, @ngrx/signals pentru state management avansat
📝 Practice Platforms
RxJS Marbles, LeetCode pentru algoritmi reactive, StackBlitz pentru experimente
🎯 Concluzie
Înțelegerea diferențelor între Observables și Signals este crucială pentru dezvoltarea eficientă în Angular modern. Observables rămân puternice pentru operații asincrone complexe, în timp ce Signals oferă o modalitate mai simplă și performantă pentru gestionarea stării locale.