Angular Signals i Computed w 2026: Precyzyjna Reaktywność i Pytania Rekrutacyjne
Opanuj Angular Signals, computed, effect i linkedSignal w Angular 20+. Wzorce precyzyjnej reaktywności oraz przygotowanie do rozmów kwalifikacyjnych.

Angular Signals reprezentują najbardziej znaczącą zmianę w modelu reaktywności Angular od powstania frameworka. Wprowadzone w Angular 17 i obecnie domyślne podejście w Angular 20+, sygnały zapewniają precyzyjną reaktywność eliminującą zbędne cykle wykrywania zmian oraz bezproblemowo integrują się z aplikacjami bezstrefowymi (zoneless).
Sygnały są synchronicznymi, wolnymi od glitchy prymitywami reaktywnymi. Computed signal przelicza wartość tylko gdy zmienią się jego zależności i tylko raz na cykl zmian, niezależnie od liczby odczytów.
Zrozumienie Prymitywów Signal w Angular 20
System sygnałów Angular składa się z trzech podstawowych prymitywów: signal(), computed() oraz effect(). Każdy służy określonemu celowi w grafie reaktywności.
Zapisywalny sygnał przechowuje wartość, którą można aktualizować za pomocą .set() lub .update(). Odczyt sygnału zwraca bieżącą wartość, a wszelkie computed signals lub effects zależne od niego są powiadamiane o zmianach.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
// Writable signal with initial value
count = signal(0);
// Computed signal derives from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() receives current value as argument
this.count.update(c => c + 1);
}
}Kluczowa różnica w stosunku do RxJS BehaviorSubject: sygnały są synchroniczne i wolne od glitchy. Gdy count się zmienia, doubleCount przelicza się dokładnie raz, nawet jeśli jest odczytywany wielokrotnie w tym samym bloku synchronicznym.
Computed Signals: Leniwe Ewaluowanie i Memoizacja
Computed signals przechowują wynik w pamięci podręcznej do momentu zmiany zależności. Ta memoizacja jest automatyczna, w przeciwieństwie do RxJS, gdzie shareReplay lub distinctUntilChanged muszą być dodane ręcznie.
import { Component, signal, computed } from '@angular/core';
interface User {
id: number;
name: string;
purchases: number[];
}
@Component({
selector: 'app-user-stats',
standalone: true,
template: `
<div>
<p>User: {{ user().name }}</p>
<p>Total spent: {{ totalSpent() | currency }}</p>
<p>Average purchase: {{ averagePurchase() | currency }}</p>
</div>
`
})
export class UserStatsComponent {
user = signal<User>({
id: 1,
name: 'Alice',
purchases: [99.99, 149.50, 29.99]
});
// Computed tracks only the properties it reads
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Depends on another computed signal
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Computed signal ewaluuje się leniwie: funkcja obliczeniowa uruchamia się dopiero przy pierwszym odczycie sygnału, nie przy deklaracji. Kolejne odczyty zwracają wartość z cache do momentu unieważnienia przez zmianę zależności.
Effect: Efekty Uboczne w Grafie Reaktywności
Effects wykonują kod w odpowiedzi na zmiany sygnałów. Uruchamiają się asynchronicznie podczas cyklu wykrywania zmian, po zakończeniu wszystkich synchronicznych aktualizacji.
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-theme-sync',
standalone: true,
template: `
<button (click)="toggleTheme()">Toggle Theme</button>
`
})
export class ThemeSyncComponent {
private document = inject(DOCUMENT);
isDarkMode = signal(false);
constructor() {
// Effect syncs signal state to DOM attribute
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect for localStorage persistence
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effects śledzą zależności dynamicznie. Jeśli gałąź warunkowa nie jest wykonywana, sygnały odczytywane tylko w tej gałęzi nie są śledzone do momentu zmiany warunku.
Używanie effect do kopiowania danych z jednego sygnału do drugiego wskazuje na problem projektowy. Preferowane jest computed dla stanu pochodnego lub linkedSignal gdy pochodna wartość musi być zapisywalna.
linkedSignal: Zapisywalny Stan Pochodny
Wprowadzony w Angular 19 i stabilny w Angular 20, linkedSignal tworzy zapisywalny sygnał, który resetuje się gdy zmienia się jego źródło. Rozwiązuje scenariusze, gdzie computed jest tylko do odczytu, ale pochodna wartość wymaga lokalnej modyfikacji.
import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({
selector: 'app-pagination',
standalone: true,
template: `
<select (change)="pageSize.set(+$any($event.target).value)">
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
<option [value]="50">50 per page</option>
</select>
<p>Page {{ currentPage() }} of {{ totalPages() }}</p>
<button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
<button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
`
})
export class PaginationComponent {
totalItems = signal(243);
pageSize = signal(10);
totalPages = computed(() =>
Math.ceil(this.totalItems() / this.pageSize())
);
// linkedSignal resets to 1 when pageSize or totalItems changes
// but allows manual navigation via prevPage/nextPage
currentPage = linkedSignal(() => 1);
prevPage(): void {
this.currentPage.update(p => Math.max(1, p - 1));
}
nextPage(): void {
this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
}
}Bez linkedSignal ten wzorzec wymagał albo effect zapisującego do innego sygnału (antywzorzec), albo złożonej orkiestracji RxJS.
Gotowy na rozmowy o Angular?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
API Komponentów Oparte na Sygnałach: input, model, viewChild
Angular 20 promuje do statusu stabilnego API komponentów oparte na sygnałach. Zastępują one dekoratory reaktywnymi alternatywami integrującymi się z grafem sygnałów.
import {
Component,
input,
model,
output,
computed,
viewChild,
ElementRef
} from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div #card class="card" [class.selected]="selected()">
<h3>{{ fullName() }}</h3>
<button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
</div>
`
})
export class UserCardComponent {
// Signal input replaces @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() creates two-way bindable signal
// Parent uses [(selected)]="parentSignal"
selected = model(false);
// Output emitter for events
cardClicked = output<void>();
// viewChild returns Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed derives from signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal inputs zapewniają lepsze bezpieczeństwo typów: input.required() zwraca InputSignal<T>, podczas gdy input() zwraca InputSignal<T | undefined> chyba że podano wartość domyślną.
Signals vs RxJS: Wydajność Pod Obciążeniem
Benchmarki w Angular 21+ pokazują, że sygnały redukują narzut wykrywania zmian w scenariuszach intensywnie wykorzystujących UI. Przy intensywnych aktualizacjach, sygnały wykazały czystsze profile pamięci i niższe zużycie CPU w porównaniu z bindingami opartymi na RxJS.
Podział odpowiedzialności jest jasny:
| Przypadek Użycia | Zalecane Podejście |
|---|---|
| Lokalny stan komponentu | signal() |
| Pochodne wartości UI | computed() |
| Synchronizacja z zewnętrznymi API | effect() |
| Żądania HTTP | RxJS Observable + toSignal() |
| Strumienie WebSocket | RxJS z operatorami |
| Debounced input użytkownika | RxJS debounceTime, następnie toSignal() |
| Złożona orkiestracja async | Pipeline RxJS |
RxJS pozostaje narzędziem do strumieni asynchronicznych, anulowania i backpressure. Sygnały obsługują stan synchroniczny.
Interoperacyjność z RxJS: toSignal i toObservable
Angular dostarcza narzędzia w @angular/core/rxjs-interop do łączenia sygnałów i observable.
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
placeholder="Search..."
/>
@if (results(); as items) {
<ul>
@for (item of items; track item.id) {
<li>{{ item.title }}</li>
}
</ul>
}
`
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// Convert signal to observable for RxJS operators
private query$ = toObservable(this.query);
// RxJS pipeline with debounce, then back to signal
results = toSignal(
this.query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => q.length > 2
? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
: []
)
),
{ initialValue: [] as SearchResult[] }
);
}toSignal subskrybuje przy tworzeniu i odsubskrybowuje przy zniszczeniu komponentu. Opcja initialValue eliminuje typ undefined z sygnału.
Pytania Rekrutacyjne: Angular Signals
Rozmowy techniczne w 2026 roku często testują zrozumienie sygnałów. Oto wzorce, które wyróżniają kandydatów na stanowiska seniorskie.
P: Co się dzieje gdy computed signal rzuca błąd?
Błąd propaguje się do każdego effect lub computed, który go odczytuje. Angular nie cachuje błędów — obliczenie ponawia się przy następnym odczycie. Kod produkcyjny powinien obsługiwać błędy wewnątrz funkcji computed.
P: Czy można zapisywać do sygnału wewnątrz computed?
Nie. Próba wywołania .set() lub .update() wewnątrz computed rzuca błąd. Funkcje computed muszą być czyste. Należy użyć linkedSignal jeśli potrzebna jest pochodna wartość, która jest również zapisywalna.
P: Jak effects obsługują odczyty sygnałów w warunkach?
// Dynamic dependency tracking example
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails is only tracked when showDetails is true
console.log('Details:', userDetails().email);
}
});Effects śledzą zależności na podstawie ostatniego wykonania. Jeśli showDetails jest false, zmiany w userDetails nie wyzwalają effect.
P: Jak działa równość sygnałów?
Domyślnie sygnały używają Object.is do porównania równości. Dla obiektów oznacza to równość referencji. Można dostarczyć własną funkcję równości:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Kandydaci wspominający o tym, że computed signals są memoizowane i leniwie ewaluowane, demonstrują zrozumienie wykraczające poza podstawowe użycie API. Dyskusja o tym, kiedy używać linkedSignal vs computed, pokazuje znajomość wzorców Angular 19+.
Wykrywanie Zmian: Sygnały i Bezstrefowy Angular
Angular 22 domyślnie używa bezstrefowego wykrywania zmian dla nowych projektów. Sygnały są tego centralnym elementem: powiadamiają Angular dokładnie kiedy zmienia się stan, eliminując potrzebę patchowania asynchronicznych API przez Zone.js.
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-zoneless-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class ZonelessCounterComponent {
count = signal(0);
increment(): void {
// Signal update triggers change detection
// No Zone.js required
this.count.update(c => c + 1);
}
}Dzięki sygnałom Angular śledzi dokładnie, które komponenty zależą od jakiego stanu. Aktualizacja sygnału planuje wykrywanie zmian tylko dla dotkniętych komponentów, nie całego drzewa.
Resource API: Dane Asynchroniczne jako Sygnały
Resource API Angular 20 (resource() i rxResource()) deklaratywnie ładuje dane asynchroniczne do sygnałów, zastępując wzorce ręcznej subskrypcji.
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Loading...</p>
}
@if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
}
@if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
userId = signal(1);
// Declarative async resource
// Refetches when userId changes
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}Resource automatycznie śledzi stan ładowania, błędy i rozwiązaną wartość. Ponownie pobiera dane gdy zmienia się sygnał request.
Najlepsze Praktyki Architektury Sygnałów
Strukturyzacja sygnałów w większych aplikacjach wymaga dyscypliny. Te wzorce skalują się od komponentów do serwisów.
Kolokacja powiązanych sygnałów: Grupowanie sygnałów, które zmieniają się razem. Komponent formularza trzyma swoje sygnały pól razem, nie rozrzucone po serwisach.
Przenoszenie współdzielonego stanu do serwisów: Gdy wiele komponentów potrzebuje tego samego stanu, sygnał należy przenieść do injectowalnego serwisu:
import { Injectable, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
// Expose read-only signal to consumers
readonly cartItems = this.items.asReadonly();
readonly totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
readonly itemCount = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
addItem(item: Omit<CartItem, 'quantity'>): void {
this.items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...items, { ...item, quantity: 1 }];
});
}
}Preferowanie computed nad effect: Jeśli wartość może być wyprowadzona, należy użyć computed. Effects służą do efektów ubocznych opuszczających graf reaktywności: manipulacja DOM, localStorage, analityka, żądania sieciowe.
Migracja z RxJS do Sygnałów
Starsze bazy kodu Angular mogą migrować stopniowo. Narzędzia interoperacyjności RxJS pozwalają sygnałom i observable współistnieć.
// Before: RxJS-based
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// After: Signal-based
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Dla operacji asynchronicznych należy zachować RxJS dla pipeline i konwertować na granicy:
// Hybrid approach
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Przygotowanie do Pytań Rekrutacyjnych o Angular Signals
- Sygnały są synchronicznymi prymitywami; RxJS obsługuje strumienie asynchroniczne
- Computed signals są leniwe i memoizowane, przeliczając tylko gdy zmieniają się zależności
- Effects uruchamiają się asynchronicznie podczas wykrywania zmian, nie natychmiast
- linkedSignal rozwiązuje problem zapisywalnego stanu pochodnego bez effects
- Signal inputs (
input(),model()) zastępują dekoratory reaktywnymi alternatywami toSignalitoObservablełączą sygnały i RxJS dla stopniowej migracji- Bezstrefowy Angular polega na sygnałach dla precyzyjnego wykrywania zmian
- Resource API (
rxResource) deklaratywnie zarządza stanem asynchronicznym - Oficjalny przewodnik Angular signals pozostaje autorytatywnym źródłem informacji o szczegółach API i najlepszych praktykach
Znajdziesz błąd w Angular?
Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Autor:
Anthony Fillion-MailletZałożyciel SharpSkill
Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.
Zaktualizowano 17 września 2026
Udostępnij
Powiązane artykuły

Zaawansowany Dependency Injection w Angular 2026: Providers, Tokens i Pytania Rekrutacyjne
Kompleksowy przewodnik po zaawansowanych mechanizmach Dependency Injection w Angular. Providers, InjectionToken, hierarchia injectorów oraz najczęstsze pytania na rozmowach kwalifikacyjnych.

Składnia Control Flow w Angular 2026: @if, @for, @switch i pytania rekrutacyjne
Kompletny przewodnik po blokowej składni przepływu sterowania w Angular. Poznaj @if, @for, @switch z praktycznymi przykładami kodu i przygotuj się do rozmów kwalifikacyjnych.

Angular HttpClient i Interceptory w 2026: Obsługa Żądań HTTP i Pytania Rekrutacyjne
Kompletny przewodnik po funkcjonalnych interceptorach Angular 20, od wstrzykiwania tokenów uwierzytelniających po logikę ponawiania żądań z wykładniczym opóźnieniem. Zawiera pytania rekrutacyjne i wzorce produkcyjne.