Angular 18: Signals und neue Features
Angular 18 Signals, zoneless Change Detection und die neuen signal-basierten APIs fuer performantere Anwendungen im Ueberblick.

Angular 18 markiert einen Wendepunkt in der Entwicklung des Frameworks mit der Stabilisierung von Signals. Dieses neue reaktive Primitiv veraendert grundlegend, wie Angular-Komponenten gebaut werden, und bietet eine moderne Alternative zu traditionellen Dekoratoren bei gleichzeitiger Vorbereitung auf Zone.js-freie Change Detection.
Die signal-basierten APIs von Angular 18: input(), model(), viewChild() und die zoneless-Konfiguration fuer leichtere und performantere Anwendungen.
Signals in Angular 18 verstehen
Signals repraesentieren einen neuen Ansatz fuer Reaktivitaet in Angular. Im Gegensatz zu klassischen Dekoratoren wie @Input(), die auf Zone.js-basierter Change Detection beruhen, bieten Signals feingranulare und explizite Reaktivitaet. Jedes Signal kapselt einen Wert und benachrichtigt Konsumenten automatisch, wenn sich dieser Wert aendert.
Dieser Ansatz bietet mehrere Vorteile: bessere Performance durch gezielte Updates, native Integration mit computed() und effect() Funktionen sowie Vorbereitung auf Angulars zoneless Zukunft.
// Demonstration of fundamental Signal concepts
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div class="counter-container">
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<button (click)="reset()">Reset</button>
</div>
`
})
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 funktionieren als reaktive Container: signal() erzeugt ein beschreibbares Signal, computed() leitet berechnete Werte ab und effect() ermoeglicht die Ausfuehrung von Aktionen als Reaktion auf Aenderungen.
Signal Inputs mit input()
Die input() Funktion ersetzt den traditionellen @Input() Dekorator. Sie gibt ein schreibgeschuetztes InputSignal zurueck und stellt sicher, dass Daten immer vom Eltern- zum Kind-Element fliessen, ohne versehentliche Modifikation.
// 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: `
<article class="book-card">
<h3>{{ book().title }}</h3>
<p class="author">By {{ book().author }}</p>
@if (hasDiscount()) {
<p class="price">
<span class="original">\${{ book().price }}</span>
<span class="discounted">\${{ discountedPrice() }}</span>
</p>
} @else {
<p class="price">\${{ book().price }}</p>
}
@if (featured()) {
<span class="badge">Featured</span>
}
</article>
`
})
export class BookCardComponent {
// Required input - template won't compile without this prop
book = input.required<Book>();
// 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);
});
}Die Verwendung im Eltern-Template bleibt aehnlich, jedoch mit Typsicherheit und Signal-Reaktivitaet:
// 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: `
<div class="book-grid">
@for (book of books(); track book.id) {
<app-book-card
[book]="book"
[featured]="book.id === featuredBookId()"
/>
}
</div>
`
})
export class BookListComponent {
books = signal<Book[]>([
{ 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');
}Der wesentliche Unterschied zu @Input(): Signal Inputs sind schreibgeschuetzt. Ein Aufruf von this.book.set() aus der Kind-Komponente ist nicht moeglich, was den unidirektionalen Datenfluss verstaerkt.
Bidirektionales Binding mit model()
Fuer Faelle, die eine bidirektionale Synchronisation erfordern, fuehrt Angular 18 model() ein. Diese Funktion erzeugt ein beschreibbares Signal, das Aenderungen automatisch an die Eltern-Komponente weitergibt.
// Component with bidirectional binding via model()
import { Component, model, output, computed } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
<div class="search-container">
<input
type="text"
[value]="query()"
(input)="onInput($event)"
[placeholder]="placeholder()"
class="search-input"
/>
@if (query().length > 0) {
<button (click)="clear()" class="clear-btn">×</button>
}
<span class="char-count">{{ charCount() }} characters</span>
</div>
`
})
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<string>();
// 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());
}
}
}Die Eltern-Komponente nutzt die Banana-in-a-Box-Syntax [()] fuer das bidirektionale Binding:
// 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: `
<div class="app-container">
<app-search-input [(query)]="searchTerm" />
<p>Current search: {{ searchTerm() }}</p>
<div class="results">
@for (result of filteredResults(); track result.id) {
<div class="result-item">{{ result.name }}</div>
}
</div>
</div>
`
})
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)
);
});
}Man verwendet input() fuer schreibgeschuetzte Daten (Eltern → Kind). model() kommt zum Einsatz, wenn die Kind-Komponente den Wert aendern muss (bidirektional).
Signal Queries mit viewChild() und contentChild()
Die Funktionen viewChild(), viewChildren(), contentChild() und contentChildren() ersetzen ihre entsprechenden Dekoratoren. Sie geben Signals zurueck und eliminieren die Notwendigkeit von Lifecycle Hooks wie ngAfterViewInit.
// 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: `
<form #formElement (submit)="onSubmit($event)">
<input #firstInput type="text" placeholder="Name" />
<app-form-field label="Email" />
<app-form-field label="Phone" />
<div class="actions">
<button type="submit">Submit</button>
<button type="button" (click)="focusFirst()">Focus first field</button>
</div>
</form>
`
})
export class FormContainerComponent {
// viewChild returns Signal<ElementRef | undefined>
formElement = viewChild<ElementRef>('formElement');
// viewChild.required guarantees element exists
firstInput = viewChild.required<ElementRef<HTMLInputElement>>('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');
}
}
}Fuer die Projektion von Inhalten und deren Zugriff funktioniert contentChild() auf aehnliche Weise:
// Using contentChild for projected content
import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<header class="card-header">
<ng-content select="[card-title]" />
</header>
<div class="card-body">
<ng-content />
</div>
@if (hasFooter()) {
<footer class="card-footer">
<ng-content select="[card-footer]" />
</footer>
}
</div>
`
})
export class CardComponent {
// Detect if footer was projected
footerContent = contentChild<ElementRef>('[card-footer]');
// Computed to check footer presence
hasFooter = computed(() => this.footerContent() !== undefined);
}Bereit für deine Angular-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Zoneless Change Detection
Angular 18 fuehrt Zone.js-freie Change Detection im experimentellen Modus ein. Dieses Feature reduziert die Bundle-Groesse um circa 13 KB und verbessert die Performance durch die Eliminierung von Monkey-Patches auf asynchronen Browser-APIs.
// 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()
]
});Die angular.json Konfiguration muss ebenfalls aktualisiert werden, um Zone.js zu entfernen:
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"polyfills": []
}
}
}
}
}
}Im zoneless Modus wird Change Detection automatisch in diesen Faellen ausgeloest: Signal-Update, markForCheck()-Aufruf, neuer Wert ueber AsyncPipe empfangen oder Komponente anhaengen/abhaengen.
// 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: `
<div class="counter">
<p>Counter: {{ count() }}</p>
<button (click)="increment()">Increment</button>
@if (loading()) {
<p>Loading...</p>
}
@if (data()) {
<pre>{{ data() | json }}</pre>
}
</div>
`
})
export class ZonelessCounterComponent {
private http = inject(HttpClient);
count = signal(0);
loading = signal(false);
data = signal<any>(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);
}
}
}Komponenten mit ChangeDetectionStrategy.OnPush und Signals sind in der Regel mit dem zoneless Modus kompatibel. Direkte Modifikationen von Eigenschaften, die keine Signals sind, sollten vermieden werden.
Bestehende Komponenten migrieren
Die Migration zu signal-basierten APIs kann schrittweise erfolgen. Hier ein Beispiel fuer das Refactoring einer traditionellen Komponente:
// 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: `
<div #container>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserProfileLegacyComponent implements AfterViewInit {
@Input() user!: { name: string; email: string };
@ViewChild('container') container!: ElementRef;
ngAfterViewInit() {
console.log('Container ready:', this.container.nativeElement);
}
}// 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: `
<div #container>
<h2>{{ user().name }}</h2>
<p>{{ user().email }}</p>
</div>
`
})
export class UserProfileComponent {
// input.required replaces @Input() with !
user = input.required<User>();
// viewChild.required replaces @ViewChild with !
container = viewChild.required<ElementRef>('container');
constructor() {
// effect replaces ngAfterViewInit for queries
effect(() => {
console.log('Container ready:', this.container().nativeElement);
});
}
}Vorteile dieser Migration: strengere Typisierung, automatische Reaktivitaet, weniger Boilerplate-Code und Kompatibilitaet mit dem zoneless Modus.
Best Practices mit Signals
Die wichtigsten Empfehlungen, um das Beste aus Signals in Angular 18 herauszuholen:
// 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: `
<div class="cart">
<h2>Cart ({{ itemCount() }} items)</h2>
@for (item of items(); track item.id) {
<div class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.quantity }} × \${{ item.price }}</span>
<button (click)="removeItem(item.id)">Remove</button>
</div>
}
<div class="cart-total">
<strong>Total: \${{ total() }}</strong>
</div>
</div>
`
})
export class CartComponent {
// Signal for mutable data
items = signal<Product[]>([]);
// 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));
}
}Die wichtigsten Punkte im Ueberblick:
computed()fuer abgeleitete Werte verwenden, anstatt im Template neu zu berechnenupdate()gegenueberset()bevorzugen, wenn der neue Wert vom alten abhaengtuntracked()in Effects verwenden, um zirkulaere Abhaengigkeiten zu vermeiden- Immer
trackin@forSchleifen angeben, um das Rendering zu optimieren
Fazit
Angular 18 legt das Fundament fuer eine Zone.js-freie Zukunft durch Signals. Die wichtigsten Erkenntnisse:
- input() ersetzt
@Input()mit strengerer Typisierung und garantiertem Schreibschutz - model() ermoeglicht reaktives bidirektionales Binding zwischen Eltern und Kind
- viewChild() und contentChild() eliminieren die Notwendigkeit von Lifecycle Hooks
- Zoneless reduziert die Bundle-Groesse und verbessert die Performance
- computed() und effect() vervollstaendigen das reaktive Oekosystem
- Eine schrittweise Migration ist Komponente fuer Komponente moeglich
Die Einfuehrung von Signals bereitet Angular-Anwendungen auf zukuenftige Versionen vor, in denen der zoneless Modus zum Standard wird. Dieser Uebergang stellt eine kluge Investition in langfristige Wartbarkeit und Performance dar.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
