"Soll ich das jetzt als Signal oder als Observable bauen?"
Diese Frage höre ich seit dem Signals-Release in fast jedem Angular-Projekt mit Laravel-Backend. Meist gefolgt von der Vermutung, RxJS sei jetzt überflüssig. Ist es nicht.
Was sich geändert hat, ist die Standardannahme. Früher war RxJS das einzige Reaktivitäts-Werkzeug, also landete auch ein simples isLoading-Flag in einem BehaviorSubject mit Subscription, async-Pipe und Aufräum-Logik. Viel Zeremonie für einen Booleschen Wert. Signals sind für genau diese trivialen Fälle das neue Paradigma: weniger Operatoren, direktere Change-Detection, kein Subscription-Management.
Aus meiner Projekterfahrung ist die saubere Trennung wichtiger als die Werkzeugwahl selbst. Wer Signals und RxJS nach Aufgabe trennt, statt dogmatisch auf eines zu setzen, bekommt Code, den auch Kollegen ohne RxJS-Tiefenwissen lesen können. Und in einem entkoppelten Setup aus Angular-SPA und zustandsloser Laravel-API gibt es überraschend viel trivialen State und überraschend wenig echte Streams.
Die Architektur dahinter: Frontend hält State, Laravel bleibt zustandslos
Bevor wir über Signals reden, lohnt der Blick auf die Gesamtarchitektur, denn sie bestimmt, wo Zustand überhaupt entsteht. Das Angular-Frontend ist eine SPA, das Laravel-Backend eine zustandslose API, die den Datenzugriff kontrolliert. Strikte Trennung. Die Synergie liegt im hochstrukturierten TypeScript-Frontend auf der einen und dem konventionsstarken, performanten Backend auf der anderen Seite.
Für State-Management heißt das: Der gesamte clientseitige Zustand lebt im Frontend. Die API liefert Daten, hält aber keine Session-State-Logik für die UI. Ladezustände, ausgewählte Filter, Formulareingaben, optimistische Updates, all das ist Sache von Angular. Genau hier spielen Signals ihre Stärke aus, weil ein Großteil dieses Zustands lokal und synchron ist.
Mehr zur Gesamtstruktur steht im Angular + Laravel Leitfaden und im Detail unter Angular-Laravel-Architektur.
Signals: für den Zustand, der einfach nur "ist"
Ein Signal ist ein Container für einen Wert, der weiß, wer ihn liest. Ändert sich der Wert, aktualisiert Angular gezielt die abhängigen Stellen im Template. Kein markForCheck, keine Subscription, kein Teardown.
Der Klassiker ist ein Filter- oder Formularzustand. Hier ein kleiner Service, der einen Suchbegriff und ein abgeleitetes Flag verwaltet:
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ProjektFilterService {
readonly suchbegriff = signal('');
readonly nurAktive = signal(true);
// abgeleiteter Zustand, neu berechnet bei jeder Aenderung
readonly hatFilter = computed(
() => this.suchbegriff().length > 0 || this.nurAktive()
);
setSuchbegriff(wert: string): void {
this.suchbegriff.set(wert);
}
zuruecksetzen(): void {
this.suchbegriff.set('');
this.nurAktive.set(true);
}
}
Im Template lesen Sie das Signal als Funktionsaufruf, {{ suchbegriff() }}. computed hält hatFilter automatisch konsistent, ohne dass Sie irgendwo subscriben oder aufräumen müssen. Für lokale Zustände, Formularwerte und einfache Flags ist das die direkteste Variante, die Angular heute anbietet.
Was hier wichtig ist und gern übersehen wird: Es gibt nichts zu leaken. Kein Observable, keine offene Subscription, kein vergessenes unsubscribe. Der Garbage Collector räumt das Signal mit der Komponente weg. Diese Eigenschaft allein rechtfertigt Signals für den Großteil des UI-State.
RxJS: für alles, was wirklich fließt
Signals ersetzen RxJS nicht dort, wo es seine Heimat hat: asynchrone Datenströme und komplexe Event-Bündelung. Tippt ein Nutzer in ein Suchfeld, wollen Sie nicht bei jedem Tastenanschlag die API anfragen. Sie wollen debouncen, Doppelte verwerfen, alte Anfragen abbrechen. Das ist RxJS-Territorium und bleibt es.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class SucheService {
private readonly http = inject(HttpClient);
private readonly eingabe$ = new Subject<string>();
// ein echter Stream: tippen -> entprellen -> anfragen
readonly ergebnisse$ = this.eingabe$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) =>
this.http.get<Projekt[]>(`/api/projekte?suche=${q}`)
)
);
suchen(begriff: string): void {
this.eingabe$.next(begriff);
}
}
switchMap bricht eine laufende Anfrage ab, sobald eine neue Eingabe kommt. Diese Logik mit Signals nachzubauen wäre umständlich und fehleranfällig. Mein Rat: Wenn Sie über Zeit, Reihenfolge oder das Kombinieren mehrerer Quellen nachdenken, bleiben Sie bei RxJS. Wenn Sie nur einen Wert halten und ablesen, nehmen Sie ein Signal.
Der häufigste Bug: vergessene Subscriptions
Memory-Leaks in Angular entstehen fast immer an derselben Stelle. Eine Komponente subscribed auf ein Observable, wird zerstört, aber die Subscription läuft weiter. Bei jeder Neuerstellung der Komponente kommt eine weitere dazu. Irgendwann tickt es.
Es gibt zwei saubere Wege, das zu vermeiden. Der erste und für Templates bevorzugte ist die AsyncPipe: Sie subscribed selbst und räumt selbst auf, wenn die Komponente verschwindet.
<!-- AsyncPipe subscribed und unsubscribed automatisch -->
@if (suche.ergebnisse$ | async; as treffer) {
<ul>
@for (p of treffer; track p.id) {
<li>{{ p.name }}</li>
}
</ul>
}
Brauchen Sie das Observable doch im TypeScript-Code, nutzen Sie takeUntilDestroyed. Der Operator bindet die Subscription an den Lebenszyklus der Komponente und beendet sie beim Zerstören:
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { SucheService } from './suche.service';
@Component({ /* ... */ })
export class SucheComponent {
private readonly suche = inject(SucheService);
constructor() {
this.suche.ergebnisse$
.pipe(takeUntilDestroyed())
.subscribe((treffer) => {
// Verarbeitung im Code, Aufraeumen passiert automatisch
console.log(treffer.length);
});
}
}
Und genau hier liegt der unterschätzte Vorteil von Signals: Sie haben dieses Problem schlicht nicht. Keine Subscription, kein Teardown, kein Leak. Je mehr Zustand Sie auf Signals umstellen, desto kleiner wird die Fläche, auf der dieser Klassiker überhaupt entstehen kann.
Bullets zur Memory-Leak-Vermeidung:
- Im Template: AsyncPipe, sie subscribed und räumt selbst auf
- Im Code:
takeUntilDestroyedan den Destroy-Lifecycle koppeln - Bei Signals: nichts, es gibt keine Subscription zu schließen
Der Brückenschlag: Laravel-Daten als Signal laden
Jetzt das praktische Herzstück. Sie wollen eine Liste von der Laravel-API laden und im Template anzeigen. Klassisch wäre das ein Observable plus AsyncPipe. Moderner ist es, das Observable an der Service-Grenze in ein Signal zu überführen, damit der Rest der Komponente in der Signal-Welt bleibt.
Zuerst der Vertrag. Jede API-Antwort wird über ein TypeScript-Interface typisiert, niemals als any. Das Interface ist der Vertrag zwischen der Laravel-Resource und dem Angular-Frontend. Der Compiler fängt Tippfehler bei Attributnamen, die Autovervollständigung wird drastisch besser, und der Code dokumentiert sich selbst:
export interface Projekt {
id: number;
name: string;
status: 'aktiv' | 'archiviert';
erstelltAm: string;
}
Auf der Laravel-Seite liefert eine API-Resource die Daten kontrolliert und ohne 1:1-Model-Leak, sodass die JSON-Struktur exakt diesem Interface entspricht:
class ProjektResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'status' => $this->status,
'erstelltAm' => $this->created_at->toIso8601String(),
];
}
}
Mehr zu diesem Vertrag steht unter Laravel API Resources und Angular-TypeScript.
Jetzt der Angular-Service. HttpClient gehört gekapselt in eine dedizierte Service-Klasse, nicht in die Komponente. toSignal überführt das HTTP-Observable in ein Signal mit Startwert, und Sie müssen nichts mehr subscriben oder aufräumen:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Injectable({ providedIn: 'root' })
export class ProjektService {
private readonly http = inject(HttpClient);
// HTTP-Observable wird zum Signal, Startwert []
readonly projekte = toSignal(
this.http.get<Projekt[]>('/api/projekte'),
{ initialValue: [] as Projekt[] }
);
}
In der Komponente lesen Sie nur noch service.projekte(). Kein Lifecycle-Code, keine Pipe nötig. In neueren Angular-Versionen geht das mit httpResource noch eine Stufe direkter, inklusive eingebauter Status-Signale für Lade- und Fehlerzustand. Das Prinzip bleibt gleich: An der Service-Grenze von RxJS nach Signal wechseln, damit die Komponente sauber bleibt.
Zum Vergleich das klassische RxJS-Pattern, das exakt dasselbe leistet, aber den Aufräum-Aufwand ins Template verschiebt:
@Injectable({ providedIn: 'root' })
export class ProjektServiceRx {
private readonly http = inject(HttpClient);
// Observable, im Template per AsyncPipe konsumiert
readonly projekte$ = this.http.get<Projekt[]>('/api/projekte');
}
Beide Varianten sind korrekt. Die Signal-Variante ist meist die schlankere, weil sie das Subscription-Management komplett wegnimmt. Die RxJS-Variante ist die richtige Wahl, sobald Sie den Stream noch weiterverarbeiten, etwa kombinieren, retrien oder mit anderen Quellen mischen.
Eine Faustregel für den Alltag
Wenn ich in einem Code-Review entscheide, sortiere ich nach einer einzigen Frage: Hat dieser Zustand eine zeitliche Dimension? Ein ausgewählter Tab hat keine, ein Live-Tickerfeed schon. Ein Formularwert hat keine, ein entprellter Suchstrom schon.
Konkret nach Aufgabe sortiert:
Diese Trennung hält den Code lesbar und die Leak-Fläche klein. Sie müssen nicht alles auf Signals umschreiben. Sie müssen nur aufhören, RxJS für Dinge zu nutzen, die gar nicht fließen.
- Lokaler Zustand, der einfach einen Wert hält: Signal
- Abgeleiteter Wert aus anderen Zuständen:
computed - Einmaliges Laden von der Laravel-API zur Anzeige:
toSignaloderhttpResource - Entprellte Suche, abgebrochene Anfragen, kombinierte Events: RxJS
- WebSocket- oder SSE-Streams, die über Zeit Werte liefern: RxJS
Sauberes State-Management im Angular-Laravel-Stack
Signals haben RxJS nicht abgelöst, sie haben den Alltag entlastet. Lokaler Zustand wird einfacher, die Leak-Fläche kleiner, und RxJS bleibt da stark, wo es hingehört. Wenn Sie ein bestehendes Angular-Frontend auf diese Trennung umstellen oder eine neue SPA gegen eine Laravel-API aufbauen wollen, unterstütze ich Sie dabei gern. Schreiben Sie mir über das Kontaktformular, und wir schauen uns Ihre konkrete State-Architektur an.
FAQ
Häufige Fragen
Sind Signals ein Ersatz für RxJS in Angular?
Nein. Signals sind das neue Paradigma für lokale Zustände, Formularwerte und einfache Flags mit effizienterer Change-Detection. RxJS bleibt für asynchrone Datenströme und komplexe Event-Bündelung. Die beiden ergänzen sich, sie konkurrieren nicht.
Wie vermeide ich Memory-Leaks, wenn ich noch Observables nutze?
Zwei Wege. Im Template die AsyncPipe verwenden, die selbst subscribed und beim Zerstören der Komponente aufräumt. Im TypeScript-Code takeUntilDestroyed, das die Subscription an den Destroy-Lifecycle koppelt. Signals selbst haben keine Subscription und damit kein Leak-Risiko.
Wie lade ich Daten von einer Laravel-API als Signal?
Kapseln Sie HttpClient in eine dedizierte Service-Klasse und überführen Sie das HTTP-Observable mit toSignal in ein Signal mit Startwert, oder nutzen Sie in neueren Angular-Versionen httpResource mit eingebauten Status-Signalen. Die Komponente liest dann nur noch das Signal, ohne Subscription und ohne Aufräum-Code.
Brauche ich für die API-Antworten wirklich eigene Interfaces?
Ja. Typisieren Sie jede Antwort über ein vordefiniertes Interface, niemals als any. Der Compiler fängt Tippfehler bei Attributnamen, die Autovervollständigung verbessert sich deutlich und der Code wird selbstdokumentierend. Das Interface ist der Vertrag zwischen Laravel-Resource und Angular-Frontend.
Gehört der HttpClient in die Komponente oder in einen Service?
In einen Service. Datenzugriff über HttpClient kapseln Sie in dedizierten Service-Klassen, ergänzt um Interceptoren für Auth-Header und Error-Handling sowie Guards für den Route-Schutz. Komponenten konsumieren nur das Ergebnis, ob als Signal oder als Observable.
Wann lohnt sich computed gegenüber einem normalen Signal?
Sobald sich ein Wert aus anderen Signalen ableitet. computed berechnet automatisch neu, wenn sich eine Abhängigkeit ändert, und cached das Ergebnis ansonsten. Sie vermeiden damit manuelle Synchronisierungslogik und halten abgeleiteten Zustand garantiert konsistent.
