typescript
49 lines · 8 steps
Optimistic UI updates in a React hook
A custom hook toggles a todo instantly, then reconciles or rolls back based on the server's response.
Explained by
highlit
1import { useState, useCallback } from 'react';
2
3type Todo = {
4 id: string;
5 title: string;
6 completed: boolean;
7};
8
9export function useToggleTodo(initial: Todo[]) {
10 const [todos, setTodos] = useState(initial);
11 const [error, setError] = useState<string | null>(null);
12
13 const toggle = useCallback(async (id: string) => {
14 const previous = todos;
15 const target = todos.find((t) => t.id === id);
16 if (!target) return;
17
18 const nextCompleted = !target.completed;
19
20 setTodos((current) =>
21 current.map((t) =>
22 t.id === id ? { ...t, completed: nextCompleted } : t,
23 ),
24 );
25 setError(null);
26
27 try {
28 const res = await fetch(`/api/todos/${id}`, {
29 method: 'PATCH',
30 headers: { 'Content-Type': 'application/json' },
31 body: JSON.stringify({ completed: nextCompleted }),
32 });
33
34 if (!res.ok) {
35 throw new Error(`Request failed: ${res.status}`);
36 }
37
38 const saved: Todo = await res.json();
39 setTodos((current) =>
40 current.map((t) => (t.id === id ? saved : t)),
41 );
42 } catch (err) {
43 setTodos(previous);
44 setError(err instanceof Error ? err.message : 'Update failed');
45 }
46 }, [todos]);
47
48 return { todos, error, toggle };
49}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Optimistic updates apply the change immediately and treat the network round-trip as confirmation, not a prerequisite.
- 2Capturing prior state before mutating gives you a clean rollback path when the request fails.
- 3Returning both data and error from a hook lets components render success and failure from one source of truth.
Related explainers
typescript
import { CanActivate, ExecutionContext, Injectable,
How guards chain auth in NestJS
guards
authentication
authorization
Intermediate
8 steps
ruby
require "net/http" require "json" class UrlFetcher
A thread-pool URL fetcher in Ruby
concurrency
thread-pool
queues
Intermediate
8 steps
go
package auth import ( "errors"
Hashing and verifying passwords with bcrypt in Go
password-hashing
bcrypt
error-handling
Intermediate
7 steps
rust
use std::fs::File; use std::io::{self, Read}; use std::path::Path;
Streaming a SHA-256 hash of a file in Rust
hashing
buffered-io
streaming
Intermediate
5 steps
go
package fsutil import ( "io/fs"
Walking a directory tree to find large files in Go
filesystem
recursion
closures
Intermediate
7 steps
typescript
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { toObservable } from '@angular/core/rxjs-interop';
Virtual scrolling a contact list in Angular
virtual-scrolling
signals
change-detection
Intermediate
6 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-ui-updates-in-a-react-hook-explained-typescript-00c6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.