S

Signals

Type-safe Angular Signals for reactive state management with computed values and effects.

Code

typescript
1// counter.component.ts - Basic signals
2import { Component, signal, computed, effect } from "@angular/core";
3
4@Component({
5 selector: "app-counter",
6 standalone: true,
7 template: `
8 <div>
9 <h2>Count: {{ count() }}</h2>
10 <h3>Doubled: {{ doubledCount() }}</h3>
11 <p>{{ message() }}</p>
12
13 <button (click)="increment()">+</button>
14 <button (click)="decrement()">-</button>
15 <button (click)="reset()">Reset</button>
16 </div>
17 `,
18})
19export class CounterComponent {
20 // Writable signal with initial value
21 count = signal(0);
22
23 // Computed signal (derived state)
24 doubledCount = computed(() => this.count() * 2);
25
26 message = computed(() => {
27 const c = this.count();
28 if (c === 0) return "Start counting!";
29 if (c > 10) return "That's a lot!";
30 if (c < 0) return "Going negative?";
31 return `Current count: ${c}`;
32 });
33
34 constructor() {
35 // Effect runs whenever signals it reads change
36 effect(() => {
37 console.log(`Count changed to: ${this.count()}`);
38 // Side effects: logging, localStorage, etc.
39 localStorage.setItem("count", this.count().toString());
40 });
41 }
42
43 increment() {
44 this.count.update((c) => c + 1);
45 }
46
47 decrement() {
48 this.count.update((c) => c - 1);
49 }
50
51 reset() {
52 this.count.set(0);
53 }
54}
55
56// user-store.ts - State management with signals
57import { Injectable, signal, computed } from "@angular/core";
58
59interface User {
60 id: number;
61 name: string;
62 email: string;
63}
64
65interface UserState {
66 users: User[];
67 selectedUserId: number | null;
68 loading: boolean;
69 error: string | null;
70}
71
72@Injectable({
73 providedIn: "root",
74})
75export class UserStore {
76 // Private writable state
77 private state = signal<UserState>({
78 users: [],
79 selectedUserId: null,
80 loading: false,
81 error: null,
82 });
83
84 // Public readonly selectors (computed signals)
85 readonly users = computed(() => this.state().users);
86 readonly loading = computed(() => this.state().loading);
87 readonly error = computed(() => this.state().error);
88
89 readonly selectedUser = computed(() => {
90 const state = this.state();
91 return state.users.find((u) => u.id === state.selectedUserId) ?? null;
92 });
93
94 readonly userCount = computed(() => this.state().users.length);
95
96 readonly adminUsers = computed(() =>
97 this.state().users.filter((u) => u.email.includes("admin"))
98 );
99
100 // Actions
101 setLoading(loading: boolean) {
102 this.state.update((s) => ({ ...s, loading }));
103 }
104
105 setError(error: string | null) {
106 this.state.update((s) => ({ ...s, error, loading: false }));
107 }
108
109 setUsers(users: User[]) {
110 this.state.update((s) => ({ ...s, users, loading: false, error: null }));
111 }
112
113 addUser(user: User) {
114 this.state.update((s) => ({
115 ...s,
116 users: [...s.users, user],
117 }));
118 }
119
120 updateUser(id: number, updates: Partial<User>) {
121 this.state.update((s) => ({
122 ...s,
123 users: s.users.map((u) => (u.id === id ? { ...u, ...updates } : u)),
124 }));
125 }
126
127 removeUser(id: number) {
128 this.state.update((s) => ({
129 ...s,
130 users: s.users.filter((u) => u.id !== id),
131 selectedUserId: s.selectedUserId === id ? null : s.selectedUserId,
132 }));
133 }
134
135 selectUser(id: number | null) {
136 this.state.update((s) => ({ ...s, selectedUserId: id }));
137 }
138}
139
140// user-list.component.ts - Using the store
141@Component({
142 selector: "app-user-list",
143 standalone: true,
144 template: `
145 @if (store.loading()) {
146 <p>Loading...</p>
147 }
148
149 @if (store.error()) {
150 <p class="error">{{ store.error() }}</p>
151 }
152
153 <p>Total users: {{ store.userCount() }}</p>
154
155 <ul>
156 @for (user of store.users(); track user.id) {
157 <li
158 [class.selected]="store.selectedUser()?.id === user.id"
159 (click)="store.selectUser(user.id)"
160 >
161 {{ user.name }} ({{ user.email }})
162 </li>
163 }
164 </ul>
165
166 @if (store.selectedUser(); as user) {
167 <div class="details">
168 <h3>Selected: {{ user.name }}</h3>
169 <p>{{ user.email }}</p>
170 </div>
171 }
172 `,
173})
174export class UserListComponent {
175 constructor(public store: UserStore) {}
176}

Run this example locally

$ ng new my-app --strict