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 { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 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.