# Angular 18: Signals en nieuwe features
> Angular 18 Signals, zoneless change detection en de nieuwe signal-gebaseerde API's voor performantere applicaties.
- Published: 2026-01-18
- Updated: 2026-04-10
- Author: SharpSkill
- Tags: angular 18, angular signals, zoneless, signal inputs, reactivity
- Reading time: 12 min
---
Angular 18 markeert een keerpunt in de evolutie van het framework met de stabilisatie van Signals. Deze nieuwe reactieve primitief transformeert fundamenteel hoe Angular-componenten worden gebouwd en biedt een modern alternatief voor traditionele decorators, terwijl het de weg vrijmaakt voor Zone.js-vrije change detection.
> **Wat men zal leren**
>
> De signal-gebaseerde API's van Angular 18: input(), model(), viewChild() en de zoneless-configuratie voor lichtere en performantere applicaties.
## Signals in Angular 18 begrijpen
Signals vertegenwoordigen een nieuwe benadering van reactiviteit in Angular. In tegenstelling tot klassieke decorators zoals `@Input()` die afhankelijk zijn van Zone.js change detection, bieden Signals fijnmazige en expliciete reactiviteit. Elk Signal kapselt een waarde in en stelt consumenten automatisch op de hoogte wanneer die waarde verandert.
Deze aanpak biedt meerdere voordelen: betere prestaties door gerichte updates, native integratie met de functies `computed()` en `effect()`, en voorbereiding op de zoneless toekomst van Angular.
```typescript
// signals-basics.component.ts
// Demonstration of fundamental Signal concepts
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
Counter: {{ count() }}
Double: {{ doubleCount() }}
`
})
export class CounterComponent {
// Writable signal - value can be modified
count = signal(0);
// Computed signal - automatically derived from count
// Only recalculates when count changes
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect - executed on every count change
// Useful for side effects (logs, API calls, etc.)
effect(() => {
console.log(`New counter value: ${this.count()}`);
});
}
increment() {
// update() allows modification based on previous value
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
// set() directly replaces the value
this.count.set(0);
}
}
```
Signals werken als reactieve containers: `signal()` maakt een beschrijfbaar Signal aan, `computed()` leidt berekende waarden af en `effect()` maakt het mogelijk om acties uit te voeren als reactie op wijzigingen.
## Signal Inputs met input()
De functie `input()` vervangt de traditionele `@Input()` decorator. Het retourneert een alleen-lezen `InputSignal`, waardoor gegevens altijd van ouder naar kind stromen zonder onbedoelde modificatie.
```typescript
// book-card.component.ts
// Component using signal inputs
import { Component, input, computed } from '@angular/core';
interface Book {
id: string;
title: string;
author: string;
price: number;
discountPercent?: number;
}
@Component({
selector: 'app-book-card',
standalone: true,
template: `
{{ book().title }}
By {{ book().author }}
@if (hasDiscount()) {
\${{ book().price }}\${{ discountedPrice() }}
} @else {
\${{ book().price }}
}
@if (featured()) {
Featured
}
`
})
export class BookCardComponent {
// Required input - template won't compile without this prop
book = input.required();
// Optional input with default value
featured = input(false);
// Computed based on input - automatically recalculated
hasDiscount = computed(() => {
const discount = this.book().discountPercent;
return discount !== undefined && discount > 0;
});
// Discounted price calculation
discountedPrice = computed(() => {
const { price, discountPercent } = this.book();
if (!discountPercent) return price;
return (price * (100 - discountPercent) / 100).toFixed(2);
});
}
```
Het gebruik in het ouder-template blijft vergelijkbaar, maar met type safety en Signal-reactiviteit:
```typescript
// book-list.component.ts
// Parent component using book-card
import { Component, signal } from '@angular/core';
import { BookCardComponent } from './book-card.component';
@Component({
selector: 'app-book-list',
standalone: true,
imports: [BookCardComponent],
template: `
@for (book of books(); track book.id) {
}
`
})
export class BookListComponent {
books = signal([
{ id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 },
{ id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 }
]);
featuredBookId = signal('1');
}
```
Het belangrijkste verschil met `@Input()`: signal inputs zijn alleen-lezen. Het aanroepen van `this.book.set()` vanuit het kind-component is onmogelijk, wat de unidirectionele gegevensstroom versterkt.
## Bidirectionele binding met model()
Voor gevallen die bidirectionele synchronisatie vereisen, introduceert Angular 18 `model()`. Deze functie maakt een beschrijfbaar Signal aan dat wijzigingen automatisch doorgeeft aan het ouder-component.
```typescript
// search-input.component.ts
// Component with bidirectional binding via model()
import { Component, model, output, computed } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
`
})
export class CartComponent {
// Signal for mutable data
items = signal([]);
// Computed for derived values - avoids unnecessary recalculations
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
constructor() {
// Effect for side effects (analytics, persistence)
effect(() => {
const currentItems = this.items();
// untracked avoids creating a dependency
untracked(() => {
localStorage.setItem('cart', JSON.stringify(currentItems));
});
});
}
addItem(product: Product) {
// update() for modifications based on previous state
this.items.update(current => {
const existing = current.find(i => i.id === product.id);
if (existing) {
return current.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...current, { ...product, quantity: 1 }];
});
}
removeItem(id: string) {
this.items.update(current => current.filter(i => i.id !== id));
}
}
```
De belangrijkste punten om te onthouden:
- Gebruik `computed()` voor afgeleide waarden in plaats van herberekening in het template
- Geef de voorkeur aan `update()` boven `set()` wanneer de nieuwe waarde afhangt van de oude
- Gebruik `untracked()` in effects om circulaire afhankelijkheden te voorkomen
- Specificeer altijd `track` in `@for` lussen om het renderen te optimaliseren
## Conclusie
Angular 18 legt het fundament voor een Zone.js-vrije toekomst via Signals. De belangrijkste inzichten:
- **input()** vervangt `@Input()` met strengere typering en gegarandeerde alleen-lezen toegang
- **model()** maakt reactieve bidirectionele binding tussen ouder en kind mogelijk
- **viewChild()** en **contentChild()** elimineren de noodzaak van lifecycle hooks
- **Zoneless** verkleint de bundlegrootte en verbetert de prestaties
- **computed()** en **effect()** completeren het reactieve ecosysteem
- Geleidelijke migratie is mogelijk component voor component
Het adopteren van Signals bereidt Angular-applicaties voor op toekomstige versies waarin de zoneless modus de standaard wordt. Deze transitie vertegenwoordigt een verstandige investering in langdurige onderhoudbaarheid en prestaties.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/nl/blog/angular/angular-18-signals-new-features