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.

Angular – Das moderne Webentwicklungs-Framework
Angular ist eine Plattform fĂŒr mobile und Desktop-Webanwendungen. Von Google entwickelt, bietet es eine umfassende Lösung fĂŒr Frontend-Entwicklung.
Angular: Framework fĂŒr dynamische Single Page Applications
Angular ist eines der beliebtesten Frameworks fĂŒr moderne Webanwendungen. Erfahre, was Angular ausmacht und wie du damit durchstartest.

⚙ 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()); // 43

Dir 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 – Reactive Extensions Library fĂŒr JavaScript
RxJS ist eine Bibliothek fĂŒr reaktive Programmierung mit Observables – ideal fĂŒr asynchronen und callback-basierten Code.
RxJS: Die Reaktive Revolution in JavaScript
RxJS bringt reaktive Programmierung nach JavaScript. Lerne, wie Observables und Operatoren funktionieren.

🔄 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.

Angular input() fĂŒr Route-Parameter: Schluss mit ActivatedRoute
Mit Angular 16+ kannst du Route-Parameter direkt per input() binden – ganz ohne ActivatedRoute.

🔧 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 + map durch computed() - 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 }
);
Warum du nur noch TypeScript nutzen solltest
TypeScript bietet dir Typsicherheit, bessere Tooling-UnterstĂŒtzung und weniger Bugs.

💡 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! 🚀