Angular Signals en Computed in 2026: Fijnkorrelige Reactiviteit en Sollicitatievragen
Een diepgaande gids over Angular Signals, Computed Signals en het reactieve model in Angular 20+. Bevat best practices, RxJS-interoperabiliteit en veelgestelde technische sollicitatievragen.

Angular Signals vertegenwoordigen de meest significante verschuiving in Angular's reactiviteitsmodel sinds het ontstaan van het framework. Geïntroduceerd in Angular 17 en nu de standaardaanpak in Angular 20+, bieden signals fijnkorrelige reactiviteit die onnodige change detection-cycli elimineert en naadloos integreert met zoneless applicaties.
Signals zijn synchrone, glitch-vrije reactieve primitieven. Een computed signal herberekent alleen wanneer zijn dependencies veranderen en slechts één keer per wijzigingscyclus, ongeacht hoe vaak het wordt gelezen.
Begrip van Signal Primitieven in Angular 20
Het signal-systeem van Angular bestaat uit drie kernprimitieven: signal(), computed() en effect(). Elk dient een specifiek doel in de reactieve graaf.
Een schrijfbaar signal houdt een waarde vast die kan worden bijgewerkt met .set() of .update(). Het lezen van het signal retourneert de huidige waarde, en alle computed signals of effects die ervan afhangen worden geïnformeerd over wijzigingen.
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 {
// Schrijfbaar signal met initiële waarde
count = signal(0);
// Computed signal is afgeleid van count
// Herberekent alleen wanneer count verandert
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() ontvangt de huidige waarde als argument
this.count.update(c => c + 1);
}
}Het belangrijkste verschil met RxJS BehaviorSubject: signals zijn synchroon en glitch-vrij. Wanneer count verandert, herberekent doubleCount exact één keer, zelfs als het meerdere keren in hetzelfde synchrone blok wordt gelezen.
Computed Signals: Lazy Evaluation en Memoization
Computed signals cachen hun resultaat totdat een dependency verandert. Deze memoization gebeurt automatisch, in tegenstelling tot RxJS waar shareReplay of distinctUntilChanged handmatig moet worden toegevoegd.
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 volgt alleen de properties die het leest
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Hangt af van een ander computed signal
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Een computed signal wordt lazy geëvalueerd: de berekeningsfunctie draait alleen wanneer het signal voor het eerst wordt gelezen, niet bij declaratie. Volgende leesacties retourneren de gecachte waarde totdat een dependency deze ongeldig maakt.
Effect: Side Effects in de Reactieve Graaf
Effects voeren code uit als reactie op signal-wijzigingen. Ze draaien asynchroon tijdens de change detection-cyclus, nadat alle synchrone updates zijn voltooid.
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 synchroniseert signal-state naar DOM-attribuut
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect voor localStorage-persistentie
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effects volgen dependencies dynamisch. Als een conditionele branch niet wordt genomen, worden signals die alleen in die branch worden gelezen niet gevolgd totdat de conditie verandert.
Het gebruik van effect om data van het ene signal naar het andere te kopiëren wijst op een ontwerpprobleem. Geef de voorkeur aan computed voor afgeleide state of linkedSignal wanneer de afgeleide waarde schrijfbaar moet zijn.
linkedSignal: Schrijfbare Afgeleide State
Geïntroduceerd in Angular 19 en stabiel in Angular 20, creëert linkedSignal een schrijfbaar signal dat reset wanneer zijn bron verandert. Het lost scenario's op waarin computed alleen-lezen is maar de afgeleide waarde lokale wijziging nodig heeft.
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 reset naar 1 wanneer pageSize of totalItems verandert
// maar staat handmatige navigatie toe 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));
}
}Zonder linkedSignal vereiste dit patroon ofwel een effect die naar een ander signal schreef (anti-pattern) of complexe RxJS-orchestratie.
Klaar om je Angular gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Signal-Gebaseerde Component API's: input, model, viewChild
Angular 20 promoveert signal-gebaseerde component API's naar stabiele status. Deze vervangen decorators door reactieve alternatieven die integreren in de signal-graaf.
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 vervangt @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() creëert tweewegsbindbaar signal
// Parent gebruikt [(selected)]="parentSignal"
selected = model(false);
// Output emitter voor events
cardClicked = output<void>();
// viewChild retourneert Signal<ElementRef | undefined>
cardElement = viewChild<ElementRef>('card');
// Computed is afgeleid van signal inputs
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal inputs bieden betere type safety: input.required() retourneert InputSignal<T> terwijl input() InputSignal<T | undefined> retourneert tenzij een default wordt gegeven.
Signals vs RxJS: Performance Onder Belasting
Benchmarks in Angular 21+ tonen aan dat signals de change detection-overhead verminderen in UI-intensieve scenario's. Onder zware belasting met frequente updates toonden signals schonere geheugenprofielen en lager CPU-gebruik vergeleken met RxJS-gedreven bindings.
De scheiding van verantwoordelijkheden is duidelijk:
| Use Case | Aanbevolen Aanpak |
|---|---|
| Lokale component state | signal() |
| Afgeleide UI-waarden | computed() |
| Synchronisatie met externe API's | effect() |
| HTTP-verzoeken | RxJS Observable + toSignal() |
| WebSocket-streams | RxJS met operators |
| Gedebounced gebruikersinvoer | RxJS debounceTime, dan toSignal() |
| Complexe async-orchestratie | RxJS-pipelines |
RxJS blijft het hulpmiddel voor async streams, annulering en backpressure. Signals behandelen synchrone state.
RxJS-Interoperabiliteit: toSignal en toObservable
Angular biedt utilities in @angular/core/rxjs-interop voor het overbruggen van signals en observables.
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('');
// Converteer signal naar observable voor RxJS-operators
private query$ = toObservable(this.query);
// RxJS-pipeline met debounce, dan terug naar 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 subscribet bij creatie en unsubscribet bij vernietiging van het component. De initialValue-optie elimineert het undefined-type van het signal.
Sollicitatievragen: Angular Signals
Technische sollicitatiegesprekken in 2026 testen vaak het begrip van signals. Hier zijn patronen die senior kandidaten onderscheiden.
V: Wat gebeurt er als een computed signal een fout gooit?
De fout propageert naar elke effect of computed die het leest. Angular cachet geen fouten, de berekening wordt opnieuw geprobeerd bij de volgende leesactie. Productiecode moet fouten afhandelen binnen de computed-functie.
V: Kun je naar een signal schrijven binnen een computed?
Nee. Het aanroepen van .set() of .update() binnen een computed gooit een fout. Computed-functies moeten puur zijn. Gebruik linkedSignal als een afgeleide waarde ook schrijfbaar moet zijn.
V: Hoe behandelen effects signal-leesacties in conditionals?
// Voorbeeld van dynamische dependency-tracking
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails wordt alleen gevolgd wanneer showDetails true is
console.log('Details:', userDetails().email);
}
});Effects volgen dependencies gebaseerd op de meest recente uitvoering. Als showDetails false is, triggeren wijzigingen aan userDetails de effect niet.
V: Hoe werkt signal-gelijkheid?
Standaard gebruiken signals Object.is voor gelijkheid. Voor objecten betekent dit referentie-gelijkheid. Aangepaste gelijkheid kan worden gegeven:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Kandidaten die vermelden dat computed signals gememoized en lazy-geëvalueerd zijn, tonen begrip voorbij basis API-gebruik. Het bespreken wanneer linkedSignal vs computed te gebruiken toont vertrouwdheid met Angular 19+ patronen.
Change Detection: Signals en Zoneless Angular
Angular 22 gebruikt standaard zoneless change detection voor nieuwe projecten. Signals zijn hierbij centraal: ze informeren Angular precies wanneer state verandert, waardoor de noodzaak voor Zone.js om async API's te patchen wordt geëlimineerd.
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 triggert change detection
// Geen Zone.js vereist
this.count.update(c => c + 1);
}
}Met signals volgt Angular precies welke componenten afhangen van welke state. Een signal-update plant change detection alleen voor beïnvloede componenten, niet de hele boom.
Resource API: Async Data als Signals
De Resource API van Angular 20 (resource() en rxResource()) laadt declaratief async data in signals en vervangt handmatige subscription-patronen.
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);
// Declaratieve async resource
// Refetcht wanneer userId verandert
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}De resource volgt automatisch laadstatus, fouten en de opgeloste waarde. Het refetcht wanneer het request-signal verandert.
Best Practices voor Signal-Architectuur
Het structureren van signals in grotere applicaties vereist discipline. Deze patronen schalen van componenten naar services.
Groepeer gerelateerde signals: Groepeer signals die samen veranderen. Een formuliercomponent houdt zijn veld-signals samen, niet verspreid over services.
Til gedeelde state naar services: Wanneer meerdere componenten dezelfde state nodig hebben, verplaats het signal naar een injecteerbare service:
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[]>([]);
// Exposeer alleen-lezen signal naar 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 }];
});
}
}Geef de voorkeur aan computed boven effect: Als een waarde kan worden afgeleid, gebruik computed. Effects zijn voor side effects die de reactieve graaf verlaten: DOM-manipulatie, localStorage, analytics, netwerkverzoeken.
Migratie van RxJS naar Signals
Legacy Angular-codebases kunnen incrementeel migreren. De RxJS-interoperabiliteitsutilities staan signals en observables toe om samen te bestaan.
// Voorheen: RxJS-gebaseerd
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// Nu: Signal-gebaseerd
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Voor async operaties, behoud RxJS voor de pipeline en converteer aan de grens:
// Hybride aanpak
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Voorbereiding op Angular Signals Sollicitatievragen
- Signals zijn synchrone primitieven; RxJS behandelt async streams
- Computed signals zijn lazy en gememoized, herberekenen alleen wanneer dependencies veranderen
- Effects draaien asynchroon tijdens change detection, niet direct
- linkedSignal lost het probleem van schrijfbare afgeleide state op zonder effects
- Signal inputs (
input(),model()) vervangen decorators door reactieve alternatieven toSignalentoObservableoverbruggen signals en RxJS voor incrementele migratie- Zoneless Angular vertrouwt op signals voor fijnkorrelige change detection
- De Resource API (
rxResource) beheert async state declaratief - De officiële Angular signals-gids blijft de gezaghebbende referentie voor API-details en best practices
Zie jij de bug in Angular?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 17 september 2026
Tags
Delen
Gerelateerde artikelen

Angular 19 sollicitatievragen: Signals, SSR en onmisbare concepten
De meest voorkomende Angular 19 sollicitatievragen: Signals, incrementele hydration, zoneless change detection en nieuwe reactieve API's met codevoorbeelden en verwachte antwoorden.

Geavanceerde Angular Dependency Injection in 2026: Providers, Tokens en Sollicitatievragen
Het dependency injection systeem van Angular biedt fijnmazige controle over service-instantiatie via providers, tokens en hierarchische injectors. Deze diepgaande analyse behandelt provider-strategieen, de inject()-functie en relevante sollicitatievragen.

Angular Control Flow Syntax in 2026: @if, @for, @switch en Interviewvragen
Uitgebreide gids over de nieuwe Angular Control Flow Syntax met @if, @for en @switch. Performance-optimalisaties, Signal-integratie en veelgestelde interviewvragen voor Angular-ontwikkelaars.