TL;DR: 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! đ
Mehr Artikel entdecken
Traefik + CrowdSec: Securing Your Homelab Against Attacks đĄď¸
CrowdSec with Traefik in Docker: Automatically protect your homelab against brute-force attacks, scanners, and known malicious IPs.
Angular Signals: Reactive State Management Without RxJS đ
Angular Signals bring reactive state management without RxJS complexity. Learn signal(), computed(), and effect() for cleaner Angular code.
Logging in Angular: Ein mächtiges Werkzeug zur Fehlersuche und Ăberwachung đľď¸
Logging ist in Angular unverzichtbar. Von TypeScript-Decorators bis zu strukturierten LoggerServices â so debuggst du effizient.
Angular und TailwindCSS
Angular und TailwindCSS sind ein starkes Duo. So richtest du Tailwind in deinem Angular-Projekt ein und nutzt Utility-First CSS effektiv.
Angular input() fĂźr Route-Parameter: Schluss mit ActivatedRoute đ
Mit Angular 16+ kannst du Route-Parameter direkt per input() in deine Komponente binden â ganz ohne ActivatedRoute. So geht's!
Angular: Framework fĂźr Single Page Applicationsđ
Entdecke Angular, das leistungsstarke Framework von Google, ideal fĂźr dynamische Webanwendungen. Erfahre mehr Ăźber Komponenten, Datenbindung und Routing! đ