Optimistic updates make your app feel faster by showing changes right away before the server confirms them.
Optimistic updates pattern in NextJS
const [data, setData] = useState(initialData); async function updateData(newData) { const previousData = data; // 1. Update UI immediately setData(newData); try { // 2. Send update to server await fetch('/api/update', { method: 'POST', body: JSON.stringify(newData), headers: { 'Content-Type': 'application/json' } }); } catch (error) { // 3. Revert UI if server fails setData(previousData); } }
Update the UI state first, then send the server request.
If the server fails, revert the UI to the old state to keep data correct.
const [count, setCount] = useState(0); function like() { const previousCount = count; setCount(previousCount + 1); // update UI immediately fetch('/api/like', { method: 'POST' }).catch(() => setCount(previousCount)); // revert if error }
const [items, setItems] = useState(['apple', 'banana']); function addItem(newItem) { const previousItems = items; setItems([...previousItems, newItem]); // show new item right away fetch('/api/add', { method: 'POST', body: JSON.stringify(newItem), headers: { 'Content-Type': 'application/json' } }) .catch(() => setItems(previousItems)); // undo if error }
This Next.js component lets you add todos instantly. It updates the list right away, sends the new todo to the server, and if the server fails, it removes the new todo and shows an alert.
It uses accessible labels and keyboard support for better user experience.
import { useState } from 'react'; export default function OptimisticUpdate() { const [todos, setTodos] = useState(['Learn Next.js', 'Build app']); const [input, setInput] = useState(''); async function addTodo() { if (!input.trim()) return; const previousTodos = todos; const newTodos = [...todos, input]; setTodos(newTodos); // update UI immediately try { const res = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ todo: input }) }); if (!res.ok) throw new Error('Failed to save'); setInput(''); // clear input on success } catch { setTodos(previousTodos); // revert UI if error alert('Failed to add todo. Please try again.'); } } return ( <main> <h1>My Todos</h1> <ul> {todos.map((todo, i) => <li key={i}>{todo}</li>)} </ul> <input aria-label="New todo" value={input} onChange={e => setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && addTodo()} /> <button onClick={addTodo} aria-label="Add todo">Add</button> </main> ); }
Always handle errors to avoid showing wrong data.
Optimistic updates improve user experience but need careful rollback logic.
Use accessible labels and keyboard events for better usability.
Optimistic updates show changes immediately to make apps feel faster.
Update UI first, then confirm with server, and revert if needed.
Good error handling and accessibility improve the pattern's success.