Sending HTTP Requests (e.g. Connecting to a Database)Entry 5 of 7
Sending Data and Optimistic Updates
An optimistic update changes the UI immediately, before the server confirms, and rolls back if the request fails.
Notes
- Sending data (
POST,PUT,DELETE) happens in response to user actions, so it belongs in event handlers, not effects. - Optimistic flow:
- Save the current state.
- Update the UI as if the request succeeded.
- Send the request.
- On failure, restore the saved state and show an error.
- It makes the app feel instant, but every optimistic update needs a rollback path.
- Restoring a saved snapshot can undo other changes made while the request was in flight. For busy UIs, roll back only the affected item (e.g. filter it out by ID).
- React 19 also has a
useOptimistichook, designed for form actions and transitions.
What / Why / When
- What
Update first, confirm later.
- Why
Better perceived performance; no spinner for actions that usually succeed.
- When
Low-risk actions with a high success rate (likes, adding to a list, toggles).
- Use case
Adding a place to the user's list and saving it with a
PUTrequest.
Code compare
async function handleSelectPlace(selectedPlace: Place) { if (userPlaces.some((place) => place.id === selectedPlace.id)) return; const previousPlaces = userPlaces; // 1. save const updatedPlaces = [selectedPlace, ...userPlaces]; setUserPlaces(updatedPlaces); // 2. update the UI try { await updateUserPlaces(updatedPlaces); // 3. send } catch (err) { setUserPlaces(previousPlaces); // 4. roll back setErrorUpdatingPlaces(err instanceof Error ? err.message : "Failed to update places."); }}Targeted rollback, when adding a new item with a temporary ID:
async function addPlayer(newPlayer: { name: string }) { const tempId = crypto.randomUUID(); setPlayers((prev) => [...prev, { ...newPlayer, id: tempId }]); try { const res = await fetch("/api/players", { method: "POST", body: JSON.stringify(newPlayer), headers: { "Content-Type": "application/json" }, }); if (!res.ok) throw new Error("Failed to save player."); } catch (err) { setPlayers((prev) => prev.filter((p) => p.id !== tempId)); // remove only this item setError(err instanceof Error ? err.message : "Failed to save player."); }}The request and the optimistic logic both live in the service. The component only calls userPlacesService.addPlace(place) and reads userPlacesService.places().
import { Injectable, inject, signal } from "@angular/core";import { HttpClient } from "@angular/common/http";@Injectable({ providedIn: "root" })export class UserPlacesService { private http = inject(HttpClient); private readonly _places = signal<Place[]>([]); private readonly _error = signal<string | null>(null); readonly places = this._places.asReadonly(); readonly error = this._error.asReadonly(); addPlace(place: Place) { const previous = this._places(); // 1. save if (previous.some((p) => p.id === place.id)) return; const updated = [place, ...previous]; this._places.set(updated); // 2. update the UI this.http.put("http://localhost:3000/user-places", updated).subscribe({ // 3. send error: () => { this._places.set(previous); // 4. roll back this._error.set("Failed to update user data."); }, }); }}Differences and gotchas
- The pattern is identical: save, update, send, roll back. React uses
setState; Angular usessignal.set()orupdate(). - If the server returns the saved item (with a real ID), replace the temporary item with it on success in both frameworks.
Quick reference
| Step | React | Angular |
|---|---|---|
| Update the UI | setX(...) | signal.set() / update() |
| Send | await fetch(...) | http.put(...).subscribe() |
| Roll back | setX(previous) in catch | signal.set(previous) in error |
| Built-in helper | useOptimistic (forms/actions) | — |