# 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: `
@if (query().length > 0) { } {{ charCount() }} characters
` }) export class SearchInputComponent { // model() creates a bidirectional Signal // Modifications propagate to parent query = model(''); // Classic input for configuration placeholder = model('Search...'); // Output for additional events searchSubmitted = output(); // Computed based on model charCount = computed(() => this.query().length); onInput(event: Event) { const value = (event.target as HTMLInputElement).value; // Update model - propagates to parent this.query.set(value); } clear() { this.query.set(''); } submit() { if (this.query().length > 0) { this.searchSubmitted.emit(this.query()); } } } ``` Het ouder-component gebruikt de banana-in-a-box-syntaxis `[()]` voor bidirectionele binding: ```typescript // app.component.ts // Using two-way binding with model() import { Component, signal, effect } from '@angular/core'; import { SearchInputComponent } from './search-input.component'; @Component({ selector: 'app-root', standalone: true, imports: [SearchInputComponent], template: `

Current search: {{ searchTerm() }}

@for (result of filteredResults(); track result.id) {
{{ result.name }}
}
` }) export class AppComponent { // Local signal synchronized with child component searchTerm = signal(''); results = signal([ { id: 1, name: 'Angular 18' }, { id: 2, name: 'React 19' }, { id: 3, name: 'Vue 3' } ]); // Reactive filtering based on searchTerm filteredResults = computed(() => { const term = this.searchTerm().toLowerCase(); if (!term) return this.results(); return this.results().filter(r => r.name.toLowerCase().includes(term) ); }); } ``` > **model() vs input()** > > Gebruik `input()` voor alleen-lezen gegevens (ouder → kind). Gebruik `model()` wanneer het kind-component de waarde moet kunnen wijzigen (bidirectioneel). ## Signal Queries met viewChild() en contentChild() De functies `viewChild()`, `viewChildren()`, `contentChild()` en `contentChildren()` vervangen hun corresponderende decorators. Ze retourneren Signals, waardoor lifecycle hooks zoals `ngAfterViewInit` overbodig worden. ```typescript // form-container.component.ts // Demonstration of signal queries import { Component, viewChild, viewChildren, ElementRef, effect, signal } from '@angular/core'; import { FormFieldComponent } from './form-field.component'; @Component({ selector: 'app-form-container', standalone: true, imports: [FormFieldComponent], template: `
` }) export class FormContainerComponent { // viewChild returns Signal formElement = viewChild('formElement'); // viewChild.required guarantees element exists firstInput = viewChild.required>('firstInput'); // Query on a component - returns the component itself firstFormField = viewChild(FormFieldComponent); // viewChildren for multiple elements allFormFields = viewChildren(FormFieldComponent); constructor() { // Effect replaces ngAfterViewInit for queries effect(() => { // Signal is automatically resolved const input = this.firstInput(); console.log('First input available:', input.nativeElement); }); // React to list changes effect(() => { const fields = this.allFormFields(); console.log(`${fields.length} form fields found`); }); } focusFirst() { // Direct access via Signal this.firstInput().nativeElement.focus(); } onSubmit(event: Event) { event.preventDefault(); // Access the form const form = this.formElement(); if (form) { console.log('Form submitted'); } } } ``` Voor het projecteren van content en het benaderen daarvan werkt `contentChild()` op vergelijkbare wijze: ```typescript // card.component.ts // Using contentChild for projected content import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core'; @Component({ selector: 'app-card', standalone: true, template: `
@if (hasFooter()) {
}
` }) export class CardComponent { // Detect if footer was projected footerContent = contentChild('[card-footer]'); // Computed to check footer presence hasFooter = computed(() => this.footerContent() !== undefined); } ``` ## Zoneless Change Detection Angular 18 introduceert Zone.js-vrije change detection in experimentele modus. Deze functie verkleint de bundlegrootte met ongeveer 13 KB en verbetert de prestaties door het elimineren van monkey-patches op asynchrone browser-API's. ```typescript // main.ts // Configuring the application in zoneless mode import { bootstrapApplication } from '@angular/platform-browser'; import { provideExperimentalZonelessChangeDetection } from '@angular/core'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ // Enable experimental zoneless detection provideExperimentalZonelessChangeDetection() ] }); ``` De `angular.json` configuratie moet eveneens worden bijgewerkt om Zone.js te verwijderen: ```json { "projects": { "my-app": { "architect": { "build": { "options": { "polyfills": [] } } } } } } ``` In zoneless modus wordt change detection automatisch getriggerd in deze gevallen: Signal-update, `markForCheck()`-aanroep, nieuwe waarde ontvangen via AsyncPipe, of component attach/detach. ```typescript // zoneless-counter.component.ts // Component optimized for zoneless mode import { Component, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-zoneless-counter', standalone: true, // OnPush recommended for zoneless changeDetection: ChangeDetectionStrategy.OnPush, template: `

Counter: {{ count() }}

@if (loading()) {

Loading...

} @if (data()) {
{{ data() | json }}
}
` }) export class ZonelessCounterComponent { private http = inject(HttpClient); count = signal(0); loading = signal(false); data = signal(null); increment() { // Signal update triggers detection this.count.update(c => c + 1); } async fetchData() { this.loading.set(true); try { // Signals guarantee view updates const response = await fetch('/api/data'); const json = await response.json(); this.data.set(json); } finally { this.loading.set(false); } } } ``` > **Zoneless compatibiliteit** > > Componenten die `ChangeDetectionStrategy.OnPush` en Signals gebruiken zijn over het algemeen compatibel met de zoneless modus. Directe wijzigingen van eigenschappen die geen Signals zijn dienen vermeden te worden. ## Bestaande componenten migreren De migratie naar signal-gebaseerde API's kan geleidelijk plaatsvinden. Hier volgt een voorbeeld van het refactoren van een traditioneel component: ```typescript // BEFORE: Component with classic decorators // user-profile-legacy.component.ts import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-user-profile-legacy', template: `

{{ user.name }}

{{ user.email }}

` }) export class UserProfileLegacyComponent implements AfterViewInit { @Input() user!: { name: string; email: string }; @ViewChild('container') container!: ElementRef; ngAfterViewInit() { console.log('Container ready:', this.container.nativeElement); } } ``` ```typescript // AFTER: Component migrated to Signals // user-profile.component.ts import { Component, input, viewChild, ElementRef, effect } from '@angular/core'; interface User { name: string; email: string; } @Component({ selector: 'app-user-profile', standalone: true, template: `

{{ user().name }}

{{ user().email }}

` }) export class UserProfileComponent { // input.required replaces @Input() with ! user = input.required(); // viewChild.required replaces @ViewChild with ! container = viewChild.required('container'); constructor() { // effect replaces ngAfterViewInit for queries effect(() => { console.log('Container ready:', this.container().nativeElement); }); } } ``` Voordelen van deze migratie: strengere typering, automatische reactiviteit, minder boilerplate-code en compatibiliteit met de zoneless modus. ## Best Practices met Signals De belangrijkste aanbevelingen om het maximale uit Signals in Angular 18 te halen: ```typescript // best-practices.component.ts // Example of best practices with Signals import { Component, signal, computed, effect, untracked, ChangeDetectionStrategy } from '@angular/core'; interface Product { id: string; name: string; price: number; quantity: number; } @Component({ selector: 'app-cart', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

Cart ({{ itemCount() }} items)

@for (item of items(); track item.id) {
{{ item.name }} {{ item.quantity }} × \${{ item.price }}
}
Total: \${{ total() }}
` }) 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