ShaharAmir
← Back to Blog
React4 min read

React 19: useOptimistic for Instant UI Updates

Make your app feel instant with optimistic updates. Show changes immediately while the server catches up

S
Shahar Amir

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:

javascript
1234567891011121314151617
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:

javascript
123456789101112131415161718192021
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

  1. User clicks → setOptimisticLikes fires instantly
  2. UI shows new value immediately
  3. Server request happens in background
  4. When complete, setLikes syncs real state
  5. If error, optimistic state automatically reverts

Live Demo: Optimistic Like Button

import { useState, useOptimistic, useTransition } from 'react';

// Fake API call with delay
const likePost = () => new Promise(resolve => 
  setTimeout(() => resolve(true), 800)
);

export default function App() {
  const [likes, setLikes] = useState(42);
  const [liked, setLiked] = useState(false);
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
  const [isPending, startTransition] = useTransition();
  const [status, setStatus] = useState('');

  function handleLike() {
    if (liked) return;
    
    startTransition(async () => {
      setOptimisticLikes(prev => prev + 1);
      setLiked(true);
      setStatus('Syncing with server...');
      
      await likePost();
      setLikes(prev => prev + 1);
      setStatus('✓ Saved!');
      setTimeout(() => setStatus(''), 1000);
    });
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, padding: 20 }}>
      <button
        onClick={handleLike}
        style={{
          display: 'flex', alignItems: 'center', gap: 8,
          padding: '12px 24px', fontSize: 18,
          border: 'none', borderRadius: 50,
          background: liked ? '#ec4899' : '#1a1a2e',
          color: 'white', cursor: 'pointer',
          transform: liked ? 'scale(1.05)' : 'scale(1)',
          transition: 'all 0.15s'
        }}
      >
        <span>{liked ? '❤️' : '🤍'}</span>
        <span>{optimisticLikes}</span>
      </button>
      <div style={{ fontSize: 14, color: '#666', minHeight: 20 }}>
        {status}
      </div>
    </div>
  );
}

Click the button — it responds instantly while "syncing" happens in the background.

With Reducer for Complex State

For more control, use a reducer:

javascript
12345678910111213141516171819202122232425
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:

javascript
12345678910111213
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
  • Key Points

  • useOptimistic(value) — creates optimistic version of state
  • Must be called inside startTransition or an Action
  • Automatically reverts on error
  • No loading states needed
  • Works with reducers for complex updates

Make your UI feel instant. Users will love it. 🚀

#react-19#hooks#useOptimistic#performance#ux

Stay Updated 📬

Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.