Angular Signals sind die Antwort auf die Frage "Muss ich wirklich RxJS lernen, nur um einen Counter zu bauen?" - Mit signal(), computed() und effect() bekommst du reaktives State-Management, das sich anfĂŒhlt wie normales TypeScript. Einfach, typsicher und performant.
đ€ Warum brauchen wir Signals?
Hand aufs Herz: Wer hat nicht schon mal eine halbe Stunde damit verbracht, einen BehaviorSubject zu debuggen, nur um festzustellen, dass man unsubscribe() vergessen hat? RxJS ist mĂ€chtig - keine Frage. Aber fĂŒr viele alltĂ€gliche Aufgaben in Angular-Komponenten ist es wie mit Kanonen auf Spatzen zu schieĂen.
Angular hat das erkannt und mit Version 16 Signals eingefĂŒhrt. Die Idee dahinter ist simpel: Reaktives State-Management, das du verstehst, ohne vorher einen Doktortitel in Marble-Diagrams zu brauchen.
Jetzt wird's spannend: Signals sind keine Konkurrenz zu RxJS. Sie ergĂ€nzen es. Aber fĂŒr komponentenlokalen State sind sie ein absoluter Gamechanger.


âïž Die Grundlagen: signal(), computed() und effect()
signal() - Dein reaktiver Container
Ein Signal ist im Grunde ein Wrapper um einen Wert, der Angular automatisch mitteilt, wenn sich etwas Àndert. Klingt erstmal simpel - ist es auch.
import { signal } from '@angular/core';
// Signal erstellen
const count = signal(0);
// Wert lesen
console.log(count()); // 0
// Wert setzen
count.set(42);
console.log(count()); // 42
// Wert basierend auf aktuellem Wert updaten
count.update(current => current + 1);
console.log(count()); // 43Dir fÀllt wahrscheinlich auf: Kein subscribe(), kein pipe(), kein Observable. Du rufst einfach count() auf und bekommst den aktuellen Wert. Fertig. Angular trackt automatisch, wo du das Signal liest, und aktualisiert nur die betroffenen Stellen im Template.
computed() - Abgeleitete Werte
Wenn du einen Wert hast, der von anderen Signals abhĂ€ngt, kommt computed() ins Spiel. Stell dir das wie eine Excel-Formel vor: Ăndert sich ein Eingabewert, wird das Ergebnis automatisch neu berechnet.
import { signal, computed } from '@angular/core';
const firstName = signal('Max');
const lastName = signal('Mustermann');
// Automatisch aktualisiert, wenn sich firstName oder lastName Àndern
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "Max Mustermann"
firstName.set('Erika');
console.log(fullName()); // "Erika Mustermann"Das Schöne daran: computed() ist lazy. Der Wert wird erst berechnet, wenn er tatsÀchlich gelesen wird. Und er wird gecacht - wenn sich die AbhÀngigkeiten nicht geÀndert haben, wird nicht neu berechnet. Performance geschenkt.
effect() - Seiteneffekte reagieren auf Ănderungen
effect() ist fĂŒr alles, was passieren soll, wenn sich ein Signal Ă€ndert - Logging, API-Calls, LocalStorage-Updates, was auch immer.
import { signal, effect } from '@angular/core';
const theme = signal('light');
// Wird automatisch ausgefĂŒhrt, wenn sich theme Ă€ndert
effect(() => {
document.body.classList.toggle('dark-mode', theme() === 'dark');
console.log(`Theme gewechselt zu: ${theme()}`);
});
// Löst den Effect aus
theme.set('dark');
// Console: "Theme gewechselt zu: dark"Wichtig: Effects laufen im Injection-Context. Das heiĂt, du erstellst sie typischerweise im Constructor oder in Feldern deiner Komponente. Angular kĂŒmmert sich automatisch um das Cleanup, wenn die Komponente zerstört wird. Kein ngOnDestroy nötig.


đ RxJS vs. Signals - Wann nutze ich was?
Hier kommen wir zum Kern der Sache. Signals und RxJS sind keine Feinde - sie haben unterschiedliche StÀrken.
| Anwendungsfall | Signals â | RxJS â |
|---|---|---|
| Komponentenlokaler State | â Perfekt | Overkill |
| Formular-Validierung | â Einfach & direkt | Funktioniert, aber verbose |
| HTTP-Requests | Mit resource() | â HttpClient ist RxJS-basiert |
| WebSocket-Streams | Nicht geeignet | â Genau dafĂŒr gemacht |
| Komplexe Event-Ketten | Nicht geeignet | â switchMap, debounceTime & Co |
| Globaler App-State | â Mit Services | Funktioniert auch |
| UI-Toggle/Counter | â Trivial | Unnötig komplex |
Faustregel: Wenn du einen Wert hast, der sich Ă€ndert und du willst, dass die UI reagiert â Signal. Wenn du mit Streams, Timing oder komplexen Event-Ketten arbeitest â RxJS.
đ» Praxisbeispiel: Todo-App mit Signals
Genug Theorie. Lass uns eine kleine Todo-Komponente bauen, die zeigt, wie Signals in der Praxis aussehen.
import { Component, signal, computed } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Todo {
id: number;
text: string;
done: boolean;
}
@Component({
selector: 'app-todo',
standalone: true,
imports: [FormsModule],
template: `
<h2>Meine Todos ({{ openCount() }} offen)</h2>
<input [(ngModel)]="newTodoText"
(keyup.enter)="addTodo()"
placeholder="Neues Todo..." />
<button (click)="addTodo()">HinzufĂŒgen</button>
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li [class.done]="todo.done">
<input type="checkbox"
[checked]="todo.done"
(change)="toggleTodo(todo.id)" />
{{ todo.text }}
<button (click)="removeTodo(todo.id)">đïž</button>
</li>
}
</ul>
<div class="filters">
<button (click)="filter.set('all')">Alle</button>
<button (click)="filter.set('open')">Offen</button>
<button (click)="filter.set('done')">Erledigt</button>
</div>
`
})
export class TodoComponent {
// State als Signals
todos = signal<Todo[]>([]);
filter = signal<'all' | 'open' | 'done'>('all');
newTodoText = '';
private nextId = 1;
// Computed: Gefilterte Todos
filteredTodos = computed(() => {
const currentFilter = this.filter();
const allTodos = this.todos();
switch (currentFilter) {
case 'open': return allTodos.filter(t => !t.done);
case 'done': return allTodos.filter(t => t.done);
default: return allTodos;
}
});
// Computed: Anzahl offener Todos
openCount = computed(() =>
this.todos().filter(t => !t.done).length
);
addTodo() {
if (!this.newTodoText.trim()) return;
this.todos.update(todos => [
...todos,
{ id: this.nextId++, text: this.newTodoText.trim(), done: false }
]);
this.newTodoText = '';
}
toggleTodo(id: number) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
}
removeTodo(id: number) {
this.todos.update(todos => todos.filter(t => t.id !== id));
}
}Schau dir an, wie sauber das ist. Kein einziges subscribe(), kein async-Pipe, keine Memory-Leak-Gefahr. Der State lebt in Signals, die abgeleiteten Werte in computed(), und Angular weiĂ automatisch, was es neu rendern muss.
đ linkedSignal() und resource() - Die neuen APIs
Angular entwickelt sich weiter, und mit neueren Versionen kommen zwei spannende APIs dazu, die Signals noch mÀchtiger machen.
linkedSignal() - Signals die voneinander abhÀngen
linkedSignal() erstellt ein beschreibbares Signal, das sich automatisch zurĂŒcksetzt, wenn sich eine Quelle Ă€ndert. Perfekt fĂŒr "abhĂ€ngige Defaults".
import { signal, linkedSignal } from '@angular/core';
const products = signal(['Laptop', 'Tablet', 'Phone']);
// WÀhlt automatisch das erste Produkt, wenn sich die Liste Àndert
const selectedProduct = linkedSignal(() => products()[0]);
console.log(selectedProduct()); // "Laptop"
// Manuell Ànderbar
selectedProduct.set('Tablet');
console.log(selectedProduct()); // "Tablet"
// Wenn sich die Quelle Ă€ndert, wird der Wert zurĂŒckgesetzt
products.set(['Monitor', 'Keyboard', 'Mouse']);
console.log(selectedProduct()); // "Monitor"Der Unterschied zu computed(): Ein linkedSignal ist beschreibbar. Du kannst den Wert manuell ĂŒberschreiben, aber er resettet sich, wenn die Quelle sich Ă€ndert. Das ist extrem nĂŒtzlich fĂŒr Dropdown-Selections, Pagination oder Filter, die an eine Datenquelle gebunden sind.
resource() - Asynchrone Daten laden
resource() verbindet Signals mit asynchronen Operationen. Statt HttpClient mit RxJS-Pipes zu nutzen, kannst du Daten direkt signal-basiert laden.
import { signal, resource } from '@angular/core';
const userId = signal(1);
const userResource = resource({
request: () => ({ id: userId() }),
loader: async ({ request }) => {
const response = await fetch(
`https://api.example.com/users/${request.id}`
);
return response.json();
}
});
// Im Template:
// @if (userResource.isLoading()) {
// <p>Laden...</p>
// }
// @if (userResource.value()) {
// <p>{{ userResource.value().name }}</p>
// }
// @if (userResource.error()) {
// <p>Fehler: {{ userResource.error() }}</p>
// }
// Neuen User laden - triggert automatisch den Loader
userId.set(2);Das Geniale: resource() gibt dir Loading-State, Error-Handling und automatisches Neuladen geschenkt. Ăndert sich userId, wird automatisch ein neuer Request ausgelöst. Kein switchMap nötig.
đ§ Migration: Von BehaviorSubject zu signal()
Du hast bestehenden Code mit RxJS und willst schrittweise migrieren? Kein Problem. Angular bietet Interop-Funktionen, die den Ăbergang smooth machen.
Vorher: RxJS-basiert
import { BehaviorSubject, combineLatest, map } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CartService {
private items$ = new BehaviorSubject<CartItem[]>([]);
private discount$ = new BehaviorSubject<number>(0);
totalPrice$ = combineLatest([this.items$, this.discount$]).pipe(
map(([items, discount]) => {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 - discount / 100);
})
);
addItem(item: CartItem) {
this.items$.next([...this.items$.value, item]);
}
setDiscount(percent: number) {
this.discount$.next(percent);
}
}Nachher: Signal-basiert
import { signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
private discount = signal(0);
totalPrice = computed(() => {
const subtotal = this.items().reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 - this.discount() / 100);
});
addItem(item: CartItem) {
this.items.update(current => [...current, item]);
}
setDiscount(percent: number) {
this.discount.set(percent);
}
}Weniger Code. Weniger Boilerplate. Keine Subscriptions, die man vergessen kann zu cleanen. Und das Beste: Du kannst beide AnsÀtze mischen.
Interop: toSignal() und toObservable()
FĂŒr die schrittweise Migration bietet Angular BrĂŒcken zwischen beiden Welten:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({...})
export class UserComponent {
private http = inject(HttpClient);
// Observable â Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] }
);
// Signal â Observable (falls du es brauchst)
searchTerm = signal('');
searchTerm$ = toObservable(this.searchTerm);
}toSignal() ist besonders praktisch: Es subscribed automatisch, gibt dir den letzten Wert als Signal zurĂŒck und unsubscribed beim Destroy. Der HttpClient bleibt RxJS-basiert, aber in deinem Template arbeitest du mit Signals.
Migrations-Tipps
- Schritt 1: Fang mit einfachen
BehaviorSubject-Feldern in Komponenten an - die sind am einfachsten zu ersetzen. - Schritt 2: Ersetze
combineLatest+mapdurchcomputed()- das ist fast immer ein 1:1-Tausch. - Schritt 3: Nutze
toSignal()fĂŒr HttpClient-Calls, statt manuell zu subscriben. - Schritt 4: Lass komplexe RxJS-Streams (WebSockets, Debounce-Chains) erstmal in Ruhe - die sind bei RxJS besser aufgehoben.
â ïž HĂ€ufige Stolperfallen
Bevor du loslegst, ein paar Dinge, die du wissen solltest:
1. Signals sind synchron
Anders als Observables liefern Signals immer sofort einen Wert. Das ist meistens ein Vorteil, aber wenn du auf asynchrone Daten wartest, brauchst du resource() oder toSignal() mit einem initialValue.
2. effect() sparsam einsetzen
Effects sind mĂ€chtig, aber sie können schnell zu schwer nachvollziehbarem Code fĂŒhren, wenn sie sich gegenseitig triggern. Bevorzuge computed() fĂŒr abgeleitete Werte und nutze effect() nur fĂŒr echte Seiteneffekte (DOM-Manipulation, Logging, API-Calls).
3. Immutability beachten
Signals erkennen Ănderungen durch Referenzvergleich. Das heiĂt: Ein Array mutieren funktioniert nicht - du musst ein neues Array erstellen:
// â Falsch - Signal erkennt die Ănderung nicht
const list = signal(['a', 'b']);
list().push('c'); // Nope!
// â
Richtig - Neues Array erstellen
list.update(current => [...current, 'c']);4. Equality-Funktion anpassen
StandardmĂ€Ăig nutzt Angular Object.is() zum Vergleichen. FĂŒr Objekte kannst du eine eigene Equality-Funktion mitgeben:
const user = signal(
{ name: 'Max', age: 30 },
{ equal: (a, b) => a.name === b.name && a.age === b.age }
);
đĄ Fazit
Angular Signals sind kein Hype - sie sind eine durchdachte ErgĂ€nzung, die das Framework deutlich zugĂ€nglicher macht. Du bekommst reaktives State-Management, das sich wie normales TypeScript anfĂŒhlt, ohne die Lernkurve von RxJS.
Meine Empfehlung:
- Neue Komponenten â Signals first
- Bestehender Code â Schrittweise migrieren mit
toSignal() - Komplexe Streams â RxJS bleibt dein Freund
- Und vor allem: Keine Angst vor dem Umstieg. Die API ist klein, intuitiv und macht SpaĂ.
Hast du Fragen oder eigene Erfahrungen mit Angular Signals? Schreib mir gerne in die Kommentare oder per Mail. Ich freue mich ĂŒber den Austausch! đ