useOptimistic

useOptimistic(realState, updateFn) returns [optimisticState, addOptimistic]. Render optimisticState: it equals the real state when nothing is pending, and shows your predicted value while an action is in flight. addOptimistic must be called inside a transition — in practice, inside a form action — and React 7,897 drops the optimistic value automatically when the action settles, with no extra render.

Showing a message before the server has itHTML
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
  "react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
import React, { useState, useOptimistic } from 'react';
import { createRoot } from 'react-dom/client';
function Chat() {
  const [messages, setMessages] = useState(['Hi there!']);
  const [shown, addOptimistic] = useOptimistic(messages,
    (current, text) => [...current, text + '  (sending...)']);
  async function send(formData) {
    const text = formData.get('message');
    addOptimistic(text);                       // instant, still inside a transition
    await new Promise(r => setTimeout(r, 9000));
    setMessages(prev => [...prev, text]);      // the real state catches up
  }
  return (
    <div style={{ font: '15px system-ui' }}>
      <ul>{shown.map((m, i) => <li key={i}>{m}</li>)}</ul>
      <form action={send}><input name="message" /> <button>Send</button></form>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Chat />);
setTimeout(() => { document.querySelector('input').value = 'On my way';
  document.querySelector('button').click(); }, 200);
</script>
Browser output of Listing 2.72
Browser output of 72

The capture above is taken mid-flight, which is why the second message still carries its label; once setMessages runs, the optimistic entry and the real one converge in a single render. The update function receives (currentState, optimisticValue) and is re-run against fresh data if the real state changes while the action is pending, so the preview never goes stale. If the action fails, do nothing: the optimistic value disappears on its own.