Angular Signals und Computed 2026: Fein-granulare Reaktivität und Interview-Fragen
Ein tiefgehender Leitfaden zu Angular Signals, Computed Signals und dem reaktiven Modell in Angular 20+. Enthält Best Practices, RxJS-Interoperabilität und häufige technische Interview-Fragen.

Angular Signals repräsentieren die bedeutendste Veränderung im Reaktivitätsmodell von Angular seit der Entstehung des Frameworks. Eingeführt in Angular 17 und nun der Standardansatz in Angular 20+, bieten Signals eine fein-granulare Reaktivität, die unnötige Change-Detection-Zyklen eliminiert und sich nahtlos in zoneless Anwendungen integriert.
Signals sind synchrone, glitch-freie reaktive Primitive. Ein Computed Signal berechnet sich nur neu, wenn sich seine Abhängigkeiten ändern, und nur einmal pro Änderungszyklus, unabhängig davon, wie oft es gelesen wird.
Verständnis der Signal-Primitive in Angular 20
Das Signal-System von Angular besteht aus drei Kern-Primitiven: signal(), computed() und effect(). Jedes erfüllt einen bestimmten Zweck im reaktiven Graphen.
Ein schreibbares Signal hält einen Wert, der mit .set() oder .update() aktualisiert werden kann. Das Lesen des Signals gibt den aktuellen Wert zurück, und alle Computed Signals oder Effects, die davon abhängen, werden über Änderungen benachrichtigt.
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 {
// Schreibbares Signal mit Initialwert
count = signal(0);
// Computed Signal leitet sich von count ab
// Berechnet sich nur neu, wenn count sich ändert
doubleCount = computed(() => this.count() * 2);
increment(): void {
// .update() erhält den aktuellen Wert als Argument
this.count.update(c => c + 1);
}
}Der wesentliche Unterschied zu RxJS BehaviorSubject: Signals sind synchron und glitch-frei. Wenn sich count ändert, berechnet sich doubleCount genau einmal neu, auch wenn es mehrfach im selben synchronen Block gelesen wird.
Computed Signals: Lazy Evaluation und Memoization
Computed Signals cachen ihr Ergebnis, bis sich eine Abhängigkeit ändert. Diese Memoization erfolgt automatisch, im Gegensatz zu RxJS, wo shareReplay oder distinctUntilChanged manuell hinzugefügt werden müssen.
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 trackt nur die Properties, die es liest
totalSpent = computed(() => {
const purchases = this.user().purchases;
return purchases.reduce((sum, p) => sum + p, 0);
});
// Hängt von einem anderen Computed Signal ab
averagePurchase = computed(() => {
const total = this.totalSpent();
const count = this.user().purchases.length;
return count > 0 ? total / count : 0;
});
}Ein Computed Signal wird lazy ausgewertet: Die Berechnungsfunktion läuft erst, wenn das Signal zum ersten Mal gelesen wird, nicht bei der Deklaration. Nachfolgende Lesevorgänge geben den gecachten Wert zurück, bis eine Abhängigkeit ihn invalidiert.
Effect: Seiteneffekte im reaktiven Graphen
Effects führen Code als Reaktion auf Signal-Änderungen aus. Sie laufen asynchron während des Change-Detection-Zyklus, nachdem alle synchronen Updates abgeschlossen sind.
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 synchronisiert Signal-State mit DOM-Attribut
effect(() => {
const theme = this.isDarkMode() ? 'dark' : 'light';
this.document.documentElement.setAttribute('data-theme', theme);
});
// Effect für localStorage-Persistenz
effect(() => {
localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
});
}
toggleTheme(): void {
this.isDarkMode.update(dark => !dark);
}
}Effects tracken Abhängigkeiten dynamisch. Wenn ein bedingter Branch nicht ausgeführt wird, werden Signals, die nur in diesem Branch gelesen werden, nicht getrackt, bis sich die Bedingung ändert.
Die Verwendung von effect zum Kopieren von Daten von einem Signal zu einem anderen deutet auf ein Designproblem hin. Bevorzugen Sie computed für abgeleiteten State oder linkedSignal, wenn der abgeleitete Wert schreibbar sein muss.
linkedSignal: Schreibbarer abgeleiteter State
Eingeführt in Angular 19 und stabil in Angular 20, erstellt linkedSignal ein schreibbares Signal, das sich zurücksetzt, wenn sich seine Quelle ändert. Es löst Szenarien, in denen computed schreibgeschützt ist, aber der abgeleitete Wert lokale Modifikation benötigt.
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 setzt sich auf 1 zurück, wenn pageSize oder totalItems sich ändert
// erlaubt aber manuelle Navigation über 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));
}
}Ohne linkedSignal erforderte dieses Muster entweder einen Effect, der in ein anderes Signal schreibt (Anti-Pattern), oder komplexe RxJS-Orchestrierung.
Bereit für deine Angular-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Signal-basierte Component-APIs: input, model, viewChild
Angular 20 befördert signal-basierte Component-APIs in den stabilen Status. Diese ersetzen Decorators durch reaktive Alternativen, die sich in den Signal-Graphen integrieren.
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 ersetzt @Input()
firstName = input.required<string>();
lastName = input<string>('');
// model() erstellt zwei-Wege-bindbares Signal
// Parent nutzt [(selected)]="parentSignal"
selected = model(false);
// Output emitter für Events
cardClicked = output<void>();
// viewChild gibt Signal<ElementRef | undefined> zurück
cardElement = viewChild<ElementRef>('card');
// Computed leitet sich von Signal-Inputs ab
fullName = computed(() =>
`${this.firstName()} ${this.lastName()}`.trim()
);
toggle(): void {
this.selected.update(s => !s);
this.cardClicked.emit();
}
}Signal-Inputs bieten bessere Typsicherheit: input.required() gibt InputSignal<T> zurück, während input() InputSignal<T | undefined> zurückgibt, es sei denn, ein Default wird bereitgestellt.
Signals vs RxJS: Performance unter Last
Benchmarks in Angular 21+ demonstrieren, dass Signals den Change-Detection-Overhead in UI-intensiven Szenarien reduzieren. Unter hoher Last mit häufigen Updates zeigten Signals sauberere Speicherprofile und niedrigeren CPU-Verbrauch im Vergleich zu RxJS-getriebenen Bindings.
Die Trennung der Zuständigkeiten ist klar:
| Anwendungsfall | Empfohlener Ansatz |
|---|---|
| Lokaler Component-State | signal() |
| Abgeleitete UI-Werte | computed() |
| Synchronisation mit externen APIs | effect() |
| HTTP-Requests | RxJS Observable + toSignal() |
| WebSocket-Streams | RxJS mit Operatoren |
| Debounced User-Input | RxJS debounceTime, dann toSignal() |
| Komplexe Async-Orchestrierung | RxJS-Pipelines |
RxJS bleibt das Werkzeug für async Streams, Cancellation und Backpressure. Signals handhaben synchronen State.
RxJS-Interoperabilität: toSignal und toObservable
Angular stellt Utilities in @angular/core/rxjs-interop zur Verfügung, um Signals und Observables zu verbinden.
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('');
// Signal zu Observable konvertieren für RxJS-Operatoren
private query$ = toObservable(this.query);
// RxJS-Pipeline mit Debounce, dann zurück zu 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 subscribt bei Erstellung und unsubscribt bei Zerstörung der Komponente. Die initialValue-Option eliminiert den undefined-Typ aus dem Signal.
Interview-Fragen: Angular Signals
Technische Interviews im Jahr 2026 testen häufig das Signal-Verständnis. Hier sind Muster, die Senior-Kandidaten unterscheiden.
F: Was passiert, wenn ein Computed Signal einen Fehler wirft?
Der Fehler propagiert zu jedem Effect oder Computed, das es liest. Angular cached keine Fehler, die Berechnung wird beim nächsten Lesen erneut versucht. Produktionscode sollte Fehler innerhalb der Computed-Funktion behandeln.
F: Kann man in ein Signal innerhalb eines Computed schreiben?
Nein. Der Versuch, .set() oder .update() innerhalb eines Computed aufzurufen, wirft einen Fehler. Computed-Funktionen müssen rein sein. Verwenden Sie linkedSignal, wenn ein abgeleiteter Wert auch schreibbar sein muss.
F: Wie behandeln Effects Signal-Lesevorgänge in Bedingungen?
// Beispiel für dynamisches Dependency-Tracking
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });
effect(() => {
if (showDetails()) {
// userDetails wird nur getrackt, wenn showDetails true ist
console.log('Details:', userDetails().email);
}
});Effects tracken Abhängigkeiten basierend auf der letzten Ausführung. Wenn showDetails false ist, triggern Änderungen an userDetails den Effect nicht.
F: Wie funktioniert Signal-Gleichheit?
Standardmäßig verwenden Signals Object.is für Gleichheit. Für Objekte bedeutet das Referenzgleichheit. Benutzerdefinierte Gleichheit kann bereitgestellt werden:
const user = signal(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);Kandidaten, die erwähnen, dass Computed Signals memoized und lazy-evaluated sind, demonstrieren Verständnis über die grundlegende API-Nutzung hinaus. Die Diskussion, wann linkedSignal vs computed zu verwenden ist, zeigt Vertrautheit mit Angular 19+ Mustern.
Change Detection: Signals und Zoneless Angular
Angular 22 setzt standardmäßig auf zoneless Change Detection für neue Projekte. Signals sind dabei zentral: Sie benachrichtigen Angular genau, wann sich der State ändert, was die Notwendigkeit eliminiert, dass Zone.js async APIs patcht.
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
// Kein Zone.js erforderlich
this.count.update(c => c + 1);
}
}Mit Signals trackt Angular genau, welche Komponenten von welchem State abhängen. Ein Signal-Update plant Change Detection nur für betroffene Komponenten, nicht für den gesamten Baum.
Resource API: Async-Daten als Signals
Angulars 20 Resource API (resource() und rxResource()) lädt deklarativ async Daten in Signals und ersetzt manuelle Subscription-Muster.
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);
// Deklarative async Resource
// Refetcht, wenn userId sich ändert
userResource = rxResource({
request: () => this.userId(),
loader: ({ request: id }) =>
this.http.get<User>(`/api/users/${id}`)
});
loadUser(id: number): void {
this.userId.set(id);
}
}Die Resource trackt automatisch Loading-State, Fehler und den aufgelösten Wert. Sie refetcht, wenn sich das Request-Signal ändert.
Best Practices für Signal-Architektur
Die Strukturierung von Signals in größeren Anwendungen erfordert Disziplin. Diese Muster skalieren von Komponenten zu Services.
Gruppieren Sie verwandte Signals: Gruppieren Sie Signals, die sich zusammen ändern. Eine Form-Komponente hält ihre Feld-Signals zusammen, nicht verstreut über Services.
Heben Sie geteilten State in Services: Wenn mehrere Komponenten denselben State benötigen, verschieben Sie das Signal in einen injectable 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[]>([]);
// Exponieren Sie read-only Signal für Consumer
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 }];
});
}
}Bevorzugen Sie computed gegenüber effect: Wenn ein Wert abgeleitet werden kann, verwenden Sie computed. Effects sind für Seiteneffekte, die den reaktiven Graphen verlassen: DOM-Manipulation, localStorage, Analytics, Netzwerk-Requests.
Migration von RxJS zu Signals
Legacy Angular-Codebasen können schrittweise migrieren. Die RxJS-Interop-Utilities erlauben die Koexistenz von Signals und Observables.
// Vorher: RxJS-basiert
@Component({ ... })
export class LegacyComponent {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(
map(u => u?.name ?? 'Guest')
);
}
// Nachher: Signal-basiert
@Component({ ... })
export class ModernComponent {
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
}Für async Operationen behalten Sie RxJS für die Pipeline und konvertieren an der Grenze:
// Hybrid-Ansatz
private search$ = toObservable(this.searchQuery).pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
);
results = toSignal(this.search$, { initialValue: [] });Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Vorbereitung auf Angular Signals Interview-Fragen
- Signals sind synchrone Primitive; RxJS behandelt async Streams
- Computed Signals sind lazy und memoized, berechnen sich nur neu, wenn sich Abhängigkeiten ändern
- Effects laufen asynchron während der Change Detection, nicht sofort
- linkedSignal löst das Problem des schreibbaren abgeleiteten States ohne Effects
- Signal-Inputs (
input(),model()) ersetzen Decorators durch reaktive Alternativen toSignalundtoObservableverbinden Signals und RxJS für schrittweise Migration- Zoneless Angular verlässt sich auf Signals für fein-granulare Change Detection
- Die Resource API (
rxResource) verwaltet async State deklarativ - Der offizielle Angular Signals Guide bleibt die autoritative Referenz für API-Details und Best Practices
Findest du den Bug in Angular?
Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Geschrieben von
Anthony Fillion-MailletGründer von SharpSkill
Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.
Aktualisiert am 17. September 2026
Tags
Teilen
Verwandte Artikel

Angular 19 Interviewfragen: Signals, SSR und unverzichtbare Konzepte
Die häufigsten Angular 19 Interviewfragen: Signals, inkrementelle Hydration, zoneless Change Detection und neue reaktive APIs mit Codebeispielen und erwarteten Antworten.

Fortgeschrittene Angular Dependency Injection 2026: Provider, Tokens und Interview-Fragen
Angular bietet mit Providern, InjectionTokens und hierarchischen Injektoren ein mächtiges DI-System. Diese tiefgehende Analyse behandelt Provider-Strategien, die inject()-Funktion und praxisrelevante Interview-Fragen.

Angular Control Flow Syntax 2026: @if, @for, @switch und Interviewfragen
Umfassende Anleitung zur neuen Angular Control Flow Syntax mit @if, @for und @switch. Performance-Optimierungen, Signal-Integration und häufige Interviewfragen für Angular-Entwickler.