typescript 59 lines · 9 steps

Optimistic updates with Angular signals

A signal-based Angular store keeps a todo list reactive while updating the UI ahead of the server and rolling back on failure.

Explained by highlit
1import { Injectable, computed, inject, signal } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { firstValueFrom } from 'rxjs';
4 
5export interface Todo {
6 id: string;
7 title: string;
8 done: boolean;
9}
10 
11@Injectable({ providedIn: 'root' })
12export class TodoStore {
13 private readonly http = inject(HttpClient);
14 private readonly todos = signal<Todo[]>([]);
15 
16 readonly all = this.todos.asReadonly();
17 readonly pending = computed(() => this.todos().filter((t) => !t.done));
18 readonly completedCount = computed(() => this.todos().filter((t) => t.done).length);
19 
20 async load(): Promise<void> {
21 const data = await firstValueFrom(this.http.get<Todo[]>('/api/todos'));
22 this.todos.set(data);
23 }
24 
25 async toggle(id: string): Promise<void> {
26 const snapshot = this.todos();
27 const target = snapshot.find((t) => t.id === id);
28 if (!target) return;
29 
30 const nextDone = !target.done;
31 this.todos.update((list) =>
32 list.map((t) => (t.id === id ? { ...t, done: nextDone } : t))
33 );
34 
35 try {
36 await firstValueFrom(
37 this.http.patch<Todo>(`/api/todos/${id}`, { done: nextDone })
38 );
39 } catch {
40 this.todos.set(snapshot);
41 }
42 }
43 
44 async add(title: string): Promise<void> {
45 const optimistic: Todo = { id: crypto.randomUUID(), title, done: false };
46 this.todos.update((list) => [...list, optimistic]);
47 
48 try {
49 const saved = await firstValueFrom(
50 this.http.post<Todo>('/api/todos', { title })
51 );
52 this.todos.update((list) =>
53 list.map((t) => (t.id === optimistic.id ? saved : t))
54 );
55 } catch {
56 this.todos.update((list) => list.filter((t) => t.id !== optimistic.id));
57 }
58 }
59}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A writable signal as the single source of truth lets derived computed values stay in sync automatically.
  2. 2Optimistic updates feel instant by mutating local state first and reconciling with the server response afterward.
  3. 3Capturing a snapshot before mutating gives you a clean rollback path when a request fails.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Optimistic updates with Angular signals — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code