React 19UXuseOptimisticServer ActionsForms

Mastering useOptimistic: Creating 'Lag-Free' UX for Global Applications

J
Josh W. Comeau (Inspired)
Featured Guide 25 min read

Click. Done. (Then Network)

When you like a tweet, does it wait for the server? No. The heart turns red instantly.
If your app shows a spinner for a boolean toggle, you are failing your users. React 19 gives us a primitive to fix this.

The Old Way: You have local state isLiked. When clicked, you set it to true. Then you fire the API. If it fails, you set it back to false. Managing three states (true, false, loading) manually is pain.

The New Way: useOptimistic lets you declare what the UI should look like while an async action is pending. It handles the switch, the display, and the rollback automatically.

02. Understanding useOptimistic

const [optimisticState, addOptimistic] = useOptimistic(
  state, // Source of Truth (Server Data)
  // Reducer: How to merge optimistic updates
  (currentState, optimisticValue) => {
     return [...currentState, optimisticValue];
  }
);

It takes two main arguments: the current real state (usually from props/server) and a reducer function.

When you call addOptimistic, React immediately re-renders with the result of your reducer. Once the async action finishes (and the new real state arrives via props), React automatically discards the optimistic state and uses the real one.

05. Share the Knowledge

๐Ÿš€

Did You Know?

Instagram was one of the first apps to popularize "Optimistic UI" at scale. They upload photos in the background while showing them in your feed immediately, making the app feel "impossibly fast."

โšก Interactive Playground

import React, { useOptimistic, useState, useRef } from 'react';

// ๐Ÿงช Message Component
export default function ChatOptimistic() {
  const [messages, setMessages] = useState([
     { id: 1, text: 'Hello!', sending: false },
     { id: 2, text: 'How are you?', sending: false }
  ]);
  
  // 1. Setup Optimistic State
  // When we add a message optimistically, we flag it sending: true
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
       ...state,
       { ...newMessage, sending: true, id: Math.random() } 
    ]
  );

  async function sendMessage(formData) {
    const text = formData.get('message');
    
    // 2. Trigger Optimistic Update immediately
    addOptimisticMessage({ text });
    
    // 3. Simulate Server Action Delay
    await new Promise(resolve => setTimeout(resolve, 1500));
    
    // 4. Update Real State (Server Response)
    setMessages(prev => [...prev, { id: Date.now(), text, sending: false }]);
  }

  return (
    <div className="p-6 bg-gray-900 text-white min-h-[500px] border border-gray-800 rounded-xl flex flex-col font-sans">
      <h2 className="text-2xl font-bold mb-6 text-green-400">Live Chat (Optimistic)</h2>
      
      <div className="flex-1 space-y-4 mb-6 overflow-y-auto">
         {optimisticMessages.map((msg, i) => (
             <div 
               key={i} 
               className={`p-3 rounded-lg max-w-[80%] ${
                 msg.sending 
                   ? 'bg-green-900/30 border border-green-500/50 text-green-200 ml-auto opacity-70 italic' 
                   : 'bg-gray-800 text-white ml-auto'
               }`}
             >
                {msg.text}
                {msg.sending && <span className="ml-2 text-xs"> (Sending...)</span>}
             </div>
         ))}
      </div>

      <form action={sendMessage} className="flex gap-2 border-t border-gray-800 pt-4">
         <input 
            name="message" 
            placeholder="Type a message..."
            className="flex-1 bg-gray-950 border border-gray-700 rounded-lg px-4 py-3 outline-none focus:border-green-500"
            required
            autoComplete="off"
            // Reset form hack for demo
            ref={e => e && e.form.reset}
         />
         <button className="bg-green-600 hover:bg-green-500 text-white font-bold px-6 rounded-lg transition-colors">
            Send
         </button>
      </form>
      
      <p className="mt-4 text-xs text-gray-500 text-center">
         Note: When you hit Send, the message appears INSTANTLY. The "Sending..." label simulates waiting for server confirmation (1.5s delay).
      </p>
    </div>
  );
}