React 19: useOptimistic for Instant UI Updates
Make your app feel instant with optimistic updates. Show changes immediately while the server catches up
React 19: useOptimistic for Instant UI Updates
Ever clicked a like button and waited for the server? That tiny delay feels sluggish. With useOptimistic, the UI updates instantly while the actual request happens in the background.
The Problem
Traditional approach — user waits for server:
function LikeButton({ postId, initialLikes }) { const [likes, setLikes] = useState(initialLikes); const [loading, setLoading] = useState(false);
async function handleLike() { setLoading(true); const newLikes = await api.likePost(postId); // User waits... setLikes(newLikes); setLoading(false); }
return ( <button onClick={handleLike} disabled={loading}> ❤️ {likes} {loading && '...'} </button> );}The Solution: useOptimistic
Update UI immediately, sync with server in background:
import { useOptimistic, useTransition } from 'react';
function LikeButton({ postId, initialLikes }) { const [likes, setLikes] = useState(initialLikes); const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes); const [isPending, startTransition] = useTransition();
function handleLike() { startTransition(async () => { setOptimisticLikes(prev => prev + 1); // Instant! const newLikes = await api.likePost(postId); setLikes(newLikes); // Syncs when done }); }
return ( <button onClick={handleLike}> ❤️ {optimisticLikes} </button> );}The button updates immediately. No loading state needed.
How It Works
- User clicks →
setOptimisticLikesfires instantly - UI shows new value immediately
- Server request happens in background
- When complete,
setLikessyncs real state - If error, optimistic state automatically reverts
Live Demo: Optimistic Like Button
Click the button — it responds instantly while "syncing" happens in the background.
With Reducer for Complex State
For more control, use a reducer:
function TodoList({ todos }) { const [optimisticTodos, setOptimisticTodos] = useOptimistic( todos, (state, newTodo) => [...state, { ...newTodo, pending: true }] );
async function addTodo(formData) { const newTodo = { text: formData.get('text'), id: Date.now() }; startTransition(async () => { setOptimisticTodos(newTodo); // Add immediately with pending: true await api.addTodo(newTodo); }); }
return ( <ul> {optimisticTodos.map(todo => ( <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}> {todo.text} </li> ))} </ul> );}Pending items show at 50% opacity until confirmed.
Error Handling
When the server fails, optimistic state automatically reverts:
function handleLike() { startTransition(async () => { setOptimisticLikes(prev => prev + 1); try { const newLikes = await api.likePost(postId); setLikes(newLikes); } catch (error) { // Optimistic state auto-reverts to `likes` toast.error('Failed to like post'); } });}No manual rollback needed!
When to Use
Perfect for:
- Like/upvote buttons
- Toggle switches (follow, bookmark)
- Adding items to lists
- Form submissions
- Any action where the server rarely fails
Avoid for:
- Critical data (payments, transfers)
- Actions that frequently fail
- When you need loading states
useOptimistic(value)— creates optimistic version of state- Must be called inside
startTransitionor an Action - Automatically reverts on error
- No loading states needed
- Works with reducers for complex updates
Key Points
Make your UI feel instant. Users will love it. 🚀
Stay Updated 📬
Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.