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 en Computed in 2026

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.

Belangrijkste Inzicht voor Sollicitaties

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.

counter.component.tstypescript
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.

user-stats.component.tstypescript
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.

theme-sync.component.tstypescript
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.

Anti-Pattern: Effect voor State-Afleiding

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.

pagination.component.tstypescript
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.

user-card.component.tstypescript
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 CaseAanbevolen Aanpak
Lokale component statesignal()
Afgeleide UI-waardencomputed()
Synchronisatie met externe API'seffect()
HTTP-verzoekenRxJS Observable + toSignal()
WebSocket-streamsRxJS met operators
Gedebounced gebruikersinvoerRxJS debounceTime, dan toSignal()
Complexe async-orchestratieRxJS-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.

search.component.tstypescript
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?

typescript
// 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:

typescript
const user = signal(
  { id: 1, name: 'Alice' },
  { equal: (a, b) => a.id === b.id }
);
Sollicitatie-Inzicht

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.

zoneless-counter.component.tstypescript
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.

user-profile.component.tstypescript
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:

cart.service.tstypescript
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.

typescript
// 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:

typescript
// 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
  • toSignal en toObservable overbruggen 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
Dagelijkse challenge

Zie jij de bug in Angular?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter 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

#angular
#signals
#computed
#reactivity
#interview

Delen

Gerelateerde artikelen