1. Einführung
Das Ziel dieses Tutorials ist es, ein einfaches Angular-Projekt mithilfe von Single Source of Truth (SSOT) und Signals darzustellen. Dabei soll gezeigt werden, wie sich durch den Einsatz von Signals eine reaktive und übersichtliche Architektur umsetzen lässt – ganz ohne komplexes RxJS-Handling und ohne mehrere, verstreute Zustände.
2. Warum Angular mit SSOT und Signals?
2.1. Single Source of Truth (SSOT)
2.1.1. Derzeitige Probleme
❌ Ohne zentrale Datenquelle speichern Komponenten ihre eigenen Datenkopien, was zu unterschiedlichen Ansichten derselben Information führen kann.
❌ Wenn sich Daten an mehreren Stellen ändern, muss man überall händisch aktualisieren – das ist fehleranfällig.
2.1.2. Was ist Single Source of Truth?
Das Konzept der Single Source of Truth bedeutet, dass der Zustand einer Anwendung zentral verwaltet wird, sodass alle Komponenten konsistente und synchronisierte Daten haben. Anstatt den Zustand in mehreren voneinander unabhängigen Quellen zu speichern, gibt es eine einzige, zuverlässige Quelle.

2.1.3. Lösung mit Single Source of Truth
✅ Konsistenter Zustand
-
Es gibt eine einzige Quelle, die den Zustand hält.
-
Alle Komponenten greifen auf dieselben aktuellen Daten zu.
✅ Bessere Skalierbarkeit
-
Neue Features können einfach in die zentrale State-Verwaltung integriert werden.
-
Der Code bleibt strukturiert und verständlich, auch bei großen Anwendungen.
2.2. Signals
2.2.1. Derzeitige Probleme
❌ Ineffiziente Change Detection (Performance-Problem)
-
Angular verwendet standardmäßig Zone.js, das nach jeder Benutzerinteraktion die gesamte UI auf Änderungen überprüft.
-
Die Folgen: Unnötige UI-Updates führen zu Performance-Problemen, besonders in großen Apps
❌ Komplexe State-Verwaltung
-
Ohne Signals benötigt man für reaktive Zustände oft RxJS oder NgRx, was zusätzlichen Code erfordert.
-
Die Folgen: Folge: Der Code wird komplizierter als nötig, besonders für einfache Datenflüsse mit Observables, Subscriptions und Operators
2.2.2. Was ist Signal?
Signals in Angular sind reaktive Mechanismen, die es ermöglichen, den Zustand einer Anwendung zu verwalten und Änderungen gezielt zu verfolgen. Ein Signal ist dabei ein Wrapper um einen Wert, der bei Änderungen automatisch alle abhängigen Komponenten oder Funktionen benachrichtigt. Er ändert nur die Komponente, wenn sich etwas geändert hat.

-
Signal as Producer:
-
Signal speichert sich einen Initialwert (4712)
-
Über die set-Methode kann der Wert geändert werden
-
-
Consumer
-
Eine Komponente oder Funktion, die das Signal liest.
-
Sobald der Wert des Signals sich ändert, wird der Consumer automatisch benachrichtigt (notify).
-
Der Consumer liest den aktuellen Wert des Signals lesen, wenn sich der Wert im Signal ändert
-
2.2.3. Lösung mit Signals
✅ Bessere Performance
-
Signals aktualisieren die UI, wenn Komponenten von einer Datenänderung betroffen sind
-
Dadurch werden nur die notwendigen Teile der Anwendung neu gerendert, was die Effizienz der Change Detection erhöht.

✅ Einfacherer Code und reaktive Programmierung
-
Kein kompliziertes RxJS-Handling mehr für einfache Zustandsänderungen.
-
Weniger Boilerplate-Code
2.2.4. Wichtige Arten von Signals
-
Computed Signals: Ein Computed Signal berechnet automatisch einen neuen Wert, wenn sich ein anderes Signal ändert.
import { signal, computed } from '@angular/core';
const count = signal(2);
const squared = computed(() => count() * count());
console.log(squared());
count.set(3);
console.log(squared());
Output
4 9
-
Effects: Ein Effect reagiert auf Signal-Änderungen und führt eine Aktion aus.
import { signal, effect } from '@angular/core';
const name = signal('Candice');
effect(() => {
console.log(`Hallo, ${name()}!`);
});
name.set('Oleg');
Output
Hallo, Candice! Hallo, Oleg!
-
LinkedSignals: Ein Linked Signal ermöglicht, verschiedene Signale miteinander zu verknüpfen, sodass Änderungen in einem Signal automatisch an ein anderes weitergegeben werden.
import { signal, linked } from '@angular/core';
const source = signal(10);
const linkedSignal = linked({
get: () => source(), // Liest den Wert von `source`
set: (value) => source.set(value) // Setzt den Wert in `source`
});
console.log(linkedSignal()); // 10
linkedSignal.set(20); // Ändert auch `source`
console.log(source()); // 20
Output
10 20
3. Angular Beispiel
3.2. Aufsetzen des Angular-Projekts
-
Neues Terminal öffnen und Angular-Projekt erstellen
ng new ssot-demo
Es werden folgende Fragen gestellt und einfach mit Enter weiterklicken:
✔ Which stylesheet format would you like to use? CSS [ https://developer.mozilla.org/docs/Web/CSS ] ✔ Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No
-
Abhängigkeiten und Packages herunterladen
npm install
-
derzeitiges Verzeichnis anschauen
tree -I "node_modules" (1)
1 | zeigt die Unterordner vom Ordner "node_modules" nicht an |
├── angular.json ├── package.json ├── package-lock.json ├── public │ └── favicon.ico ├── README.md ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ └── app.routes.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json
-
Starten der Angular-App
ng serve --open (1)
1 | öffnet automatisch die Angular-App |

3.3. Model
Um Daten speichern zu können, wird ein Modell erstellt, das als zentrale Struktur für den Zustand dient. Dabei holen wir die Todos über eine öffentliche API von jsonplaceholder und ergänzen das Modell um weitere einfache Felder.
-
Neues Terminal öffnen und Ordner "model" in
ssot-demo/src/app
erstellen
mkdir model
Erstellen Sie folgende *.ts
Dateien im Ordner model
:
Index
export {store} from './model'
export type {Model} from './model'
export type {Todo} from './todo'
Model
import {Todo} from "./todo";
import {signal} from "@angular/core";
export interface Model {
readonly firstName: string
readonly lastName: string
readonly todos: Todo[]
readonly todosLoaded: boolean;
}
export const initial: Model =
{
firstName: "Candice",
lastName: "Oleg",
todos: [],
todosLoaded: false
}
export const store = signal<Model>(initial); (1)
1 | die Single Source of Truth, wo wir unsere Daten zentral lagern. Als Typ wird ein Signal verwendet, damit alle Komponenten, die auf diese Daten zugreifen, automatisch informiert werden, wenn sich etwas ändert. |
Todo
export interface Todo {
userId: number
id: number
title: string
completed: boolean
}
├── angular.json ├── package.json ├── package-lock.json ├── public │ └── favicon.ico ├── README.md ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ └── model │ │ ├── index.ts │ │ ├── model.ts │ │ └── todo.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json
3.4. Service
-
Neues Terminal öffnen und Ordner
services
inssot-demo/src/app
erstellen
mkdir services cd services
-
Im Ordner
services
folgende Service erstellen:
ng g s app-state ng g s store
Output
CREATE src/app/services/app-state.service.spec.ts (368 bytes) CREATE src/app/services/app-state.service.ts (137 bytes) CREATE src/app/services/store.service.spec.ts (352 bytes) CREATE src/app/services/store.service.ts (134 bytes)
├── angular.json ├── package.json ├── package-lock.json ├── public │ └── favicon.ico ├── README.md ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── todo.ts │ │ └── services │ │ ├── app-state.service.spec.ts │ │ ├── app-state.service.ts │ │ ├── store.service.spec.ts │ │ └── store.service.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json
store
import { Injectable } from '@angular/core';
import { store } from '../model';
@Injectable({
providedIn: 'root'
})
export class StoreService {
constructor() { }
(1)
get store () {
return store
}
}
1 | Der StoreService stellt den zentralen Zustand (store) als Signal anwendungsweit zur Verfügung. |
app-state
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Model, Todo } from '../model';
import { initial } from '../model/model';
import {StoreService} from "./store.service";
@Injectable({
providedIn: 'root',
})
export class AppStateService {
// jsonplaceholder, um Fake-Daten zu holen
private readonly API_URL = 'https://jsonplaceholder.typicode.com/todos?_limit=20'; (1)
// Single Source of Truth als Signal
(2)
private state = inject(StoreService).store
// Computed Signals für Selektoren
(3)
readonly todosSignals = computed(() => this.state().todos);
readonly firstName = computed(() => this.state().firstName);
readonly lastName = computed(() => this.state().lastName);
constructor(private http: HttpClient) {}
// Zustand aktualisieren
(4)
updateState(recipe: (state: Model) => Model) {
this.state.update(recipe);
}
addTodo(todo: Todo) {
this.updateState((state) => ({
...state,
todos: [...state.todos, todo],
}));
}
toggleTodoStatus(id: number) {
this.updateState((state) => ({
...state,
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
}
removeTodo(id: number) {
this.updateState((state) => ({
...state,
todos: state.todos.filter((todo) => todo.id !== id),
}));
}
(5)
loadTodos() {
if (!this.state().todosLoaded) {
this.http
.get<Todo[]>(this.API_URL)
.subscribe((todos) => {
this.updateState((state) => ({
...state,
todos,
todosLoaded: true,
}));
});
}
}
}
AppStateService dient zum Verwalten der ganzen State (Zustand) auf Basis der SSOT, z.B. einfügen, löschen etc…
1 | Die URL für die API, um die Fake-Daten zu holen |
2 | es wird sich der store geholt (SSOT) |
3 | Es werden gezielt auf Teile des Zustands zugegriffen ⇒ wird automatisch aktualisiert, wenn sich etwas ändert. |
4 | mit recipe → die den Zustand verändert und ein neues Model-Objekt zurückgibt - vergleichbar mit einem "Reducer" in anderen Architekturen. |
5 | holt sich die Daten von jsonplaceholder |
3.5. Component
3.5.1. Overview
Es wird eine einfache Seite dargestellt, die die firstName
und lastName
anzeigt. Wenn man die Felder ändert, werden
sie sofort geändert.
-
Neues Terminal öffnen und Ordner
components
inssot-demo/src/app
erstellen
mkdir components cd components
-
Im Ordner
services
folgende Component erstellen:
ng g c overview
Output
CREATE src/app/components/overview/overview.component.css (0 bytes) CREATE src/app/components/overview/overview.component.html (23 bytes) CREATE src/app/components/overview/overview.component.spec.ts (606 bytes) CREATE src/app/components/overview/overview.component.ts (222 bytes)
├── angular.json ├── package.json ├── package-lock.json ├── public │ └── favicon.ico ├── README.md ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── components │ │ │ └── overview │ │ │ ├── overview.component.css │ │ │ ├── overview.component.html │ │ │ ├── overview.component.spec.ts │ │ │ └── overview.component.ts │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── todo.ts │ │ └── services │ │ ├── app-state.service.spec.ts │ │ ├── app-state.service.ts │ │ ├── store.service.spec.ts │ │ └── store.service.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json
Overview
<h1>Hello {{ firstName() }} {{ lastName()}}!</h1>
<form>
<fieldset>
<label>
First name
<input #name
value="{{ firstName() }}"
placeholder="First name"
autocomplete="given-name"
(input)="onFirstNameChanged(name.value)"
/>
</label>
<label>
Email
<input #last
value="{{ lastName() }}"
placeholder="lastName"
autocomplete="lastName"
(input)="onLastNameChanged(last.value)"
/>
</label>
</fieldset>
</form>
import {Component, inject} from '@angular/core';
import {AppStateService} from '../../services/app-state.service';
@Component({
selector: 'app-overview',
imports: [],
templateUrl: './overview.component.html',
styleUrl: './overview.component.css'
})
export class OverviewComponent {
(1)
state = inject(AppStateService)
firstName = inject(AppStateService).firstName;
lastName = inject(AppStateService).lastName;
(2)
onFirstNameChanged(value: string) {
this.state.updateState(state => ({
...state,
firstName: value
}));
}
onLastNameChanged(value: string) {
this.state.updateState(state => ({
...state,
lastName: value
}));
}
}
1 | Es werden nur die benötigten Daten für diese Komponente injiziert und geholt |
2 | Sie ruft updateState() aus dem AppStateService auf und ändert nur den firstName im zentralen Zustand. Das gleiche
gitl für onLastNameChanged(…) |
Um die Overview-Component
anzeigen zu können, müssen wir unsere Router und die app.component.html
konfigurieren. Die app.routes.ts
findet man in
ssot-demo/src/app
Router
import { Routes } from '@angular/router';
import {OverviewComponent} from '/components/overview/overview.component';
export const routes: Routes = [
{ path: "", redirectTo: "overview", pathMatch: "full" }, (1)
{ path: "overview", component: OverviewComponent} (2)
];
1 | Sie leitet automatisch weiter zur Route /overview . pathMatch: "full" bedeutet, es muss exakt der leere Pfad sein (/), damit es weitergeleitet wird |
2 | Wenn der User /overview in die URL eingibt, wird die OverviewComponent geladen. |
app.component.html
<header>
</header>
<main class="container">
<nav>
<ul>
<li><strong>Demonstration Signals and Single Source of Truth</strong></li>
</ul>
<ul>
<li><a [routerLink]="'/overview'">Overview</a></li> (1)
</ul>
</nav>
<router-outlet/> (2)
</main>
<footer>
</footer>
1 | routerLink: Wenn man draufklickt, lädt Angular die OverviewComponent und ersetzt den Inhalt im <router-outlet> |
2 | Platzhalter, wo Angular die jeweils passende Komponente basierend auf der aktuellen Route anzeigt |

-
Wir stoßen jedoch auf ein httpClient-Provider Fehler
-
Um den Fehler zu beheben, müssen wir in der
app.config.ts
inssot-demo/src/app
denprovideHttpClient()
hinzufügen:
app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import {provideHttpClient} from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()]
};

Um die html etwas schöner zu gestalten, müssen wir in der index.html
in ssot-demo/src
eine externe .css
importieren:
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RxJsDemo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<app-root></app-root>
</body>
</html>

3.5.2. Todo
Diese Komponente demonstriert, wie man eine Todo-Liste mit Signals und zentralem Zustand (SSOT) verwaltet – ohne RxJS. Sie zeigt, wie man Daten anzeigen, aktualisieren und verwalten kann - zentral und klar getrennt von der Logik.
-
Neues Terminal öffnen und im Ordner
components
inssot-demo/src/app
folgende Component erstellen:
ng g c todo
Output
CREATE src/app/components/todo/todo.component.css (0 bytes) CREATE src/app/components/todo/todo.component.html (19 bytes) CREATE src/app/components/todo/todo.component.spec.ts (578 bytes) CREATE src/app/components/todo/todo.component.ts (206 bytes)
├── angular.json ├── package.json ├── package-lock.json ├── public │ └── favicon.ico ├── README.md ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── components │ │ │ ├── overview │ │ │ │ ├── overview.component.css │ │ │ │ ├── overview.component.html │ │ │ │ ├── overview.component.spec.ts │ │ │ │ └── overview.component.ts │ │ │ └── todo │ │ │ ├── todo.component.css │ │ │ ├── todo.component.html │ │ │ ├── todo.component.spec.ts │ │ │ └── todo.component.ts │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── todo.ts │ │ └── services │ │ ├── app-state.service.spec.ts │ │ ├── app-state.service.ts │ │ ├── store.service.spec.ts │ │ └── store.service.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json
Todo
<div>
<h3>Todos (Signal)</h3>
<ul>
@for (todo of todos();track todo){
<li>
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodoStatus(todo.id)"
/>
{{ todo.title }}
<button (click)="deleteTodo(todo.id)">Delete</button>
</li>
}
</ul>
<div>
<input [(ngModel)]="newTodoTitle" placeholder="New Todo" />
<button (click)="addTodo()">Add Todo</button>
</div>
</div>
import {Component, OnInit} from '@angular/core';
import {AppStateService} from '../../services/app-state.service';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-todo',
imports: [
FormsModule
],
templateUrl: './todo.component.html',
styleUrl: './todo.component.css'
})
export class TodoComponent implements OnInit{
newTodoTitle = '';
constructor(private appState: AppStateService) {}
ngOnInit() {
this.appState.loadTodos(); (1)
}
get todos() {
return this.appState.todosSignals; // Getter für die todos
}
// Erstellt eine neue Todo
addTodo() {
if (this.newTodoTitle.trim()) {
this.appState.addTodo({
userId: 1,
id: Date.now(),
title: this.newTodoTitle.trim(),
completed: false,
});
this.newTodoTitle = '';
}
}
// Umschalten der Todos auf true oder false
toggleTodoStatus(id: number) {
this.appState.toggleTodoStatus(id);
}
// Löschen der Todo
deleteTodo(id: number) {
this.appState.removeTodo(id);
}
}
1 | Wenn die Komponente initialisiert wird, lädt sie die Todos, indem sie this.appState.loadTodos() aufruft |
Um diese Todo-Component
anzeigen zu können, müssen wir diese noch in der Router und in der app.component.html
konfigurieren
Router
import { Routes } from '@angular/router';
import {OverviewComponent} from './components/overview/overview.component';
import {TodoComponent} from './components/todo/todo.component';
export const routes: Routes = [
{ path: "", redirectTo: "overview", pathMatch: "full" },
{ path: "overview", component: OverviewComponent},
{ path: "todo", component: TodoComponent}
];
app.component.html
<header>
</header>
<main class="container">
<nav>
<ul>
<li><strong>Demonstration Signal and Single Source of Truth</strong></li>
</ul>
<ul>
<li><a [routerLink]="'/overview'">Overview</a></li>
<li><a [routerLink]="'/todo'">Todo</a></li>
</ul>
</nav>
<router-outlet/>
</main>
<footer>
</footer>


3.6. Beispiel: Vorgang eines Löschens von einem Todo

-
Situation: Der Benuter klickt auf den "Delete Button" in der TodoComponent (Component)
-
Es wird eine Action ausgelöst, da er die Methode deleteTodo() aufgerufen hat (Action)
-
Die AppStateService ruft in der deleteTodo() die updateState() auf. (AppStateService)
-
Mittels der Methode updateState erzeugt einen neuen Zustand, indem das Todo aus state.todos entfernt wird (Reducer)
-
der Store wird aktualisiert → alle Komponente, die todosSignals() verwenden, erhalten automatisch den neuen Zustand. (Store)
-
Der Selector passt die UI an → computed()-Signal erkennt die Änderung und informiert die Component automatisch. (Selector) Das gelöschte Todo verschwindet aus der UI ohne manuelles Refresh. (Component)
Fluss des Löschen in der Grafik:
Component → Action → AppStateService → Reducer → Store → Selector → Component aktualisiert sich automatisch.
4. Glossar
-
Signal: Reaktive Quelle von Zustandsänderungen.
-
Computed Signal: Dynamisch berechneter Wert basierend auf anderen Signals.
-
Effect Signal: Signal, das Seiteneffekte bei Änderungen auslöst.
-
Linked Signal: Signal, das von anderen Signals abhängt und sich mit deren Änderungen aktualisiert.
-
-
Single Source of Truth: Der Zustand der Anwendung wird zentral gespeichert.