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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A writable signal as the single source of truth lets derived computed values stay in sync automatically.
- 2Optimistic updates feel instant by mutating local state first and reconciling with the server response afterward.
- 3Capturing a snapshot before mutating gives you a clean rollback path when a request fails.
Related explainers
javascript
import { parsePhoneNumberFromString } from 'libphonenumber-js'; export class PhoneValidationError extends Error { constructor(message, code) {
Normalizing phone numbers with typed errors
validation
error-handling
custom-errors
Intermediate
6 steps
python
import os from datetime import datetime, timezone import jwt
JWT bearer auth as a FastAPI dependency
authentication
jwt
dependency-injection
Intermediate
7 steps
rust
use std::error::Error; use std::path::Path; use serde::Deserialize;
Custom CSV deserialization with serde in Rust
serde
csv
deserialization
Intermediate
7 steps
go
package config import ( "fmt"
Loading typed config from environment variables in Go
configuration
environment-variables
type-parsing
Beginner
8 steps
typescript
type EventMap = Record<string, unknown>; type Handler<T> = (payload: T) => void;
A type-safe event bus in TypeScript
generics
event-emitter
type-safety
Advanced
7 steps
python
import logging import traceback from flask import Blueprint, jsonify, request
Centralized JSON error handling in Flask
error-handling
blueprints
json-api
Intermediate
5 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/optimistic-updates-with-angular-signals-explained-typescript-e873/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.