Trust is earned, not given

A different perspective

2024-11-12 · Projects

React.js, part 11: useOptimistic — instant UI on top of a slow server

Part 11from the React.js series · 20 parts in all

The fastest interface is the one that never waits. Optimistic UI has always meant the same thing — show the expected result immediately, reconcile when the server answers — but doing it by hand is fiddly: you manage a shadow state, roll it back on failure, and keep it in sync. useOptimistic is React's built-in version of that pattern.

The idea in code

You derive a temporary value from the real state plus any pending actions. When the action settles (or the underlying state changes), React throws the optimistic value away and the real one takes over. There is nothing to roll back manually.

'use client';

import { useOptimistic } from 'react';
import { sendMessage } from '@/app/actions';

export function Thread({ messages }) {
  const [optimistic, addOptimistic] = useOptimistic(
    messages,
    // reducer: real state + the pending item -> what to show now
    (current, pending) => [...current, { ...pending, sending: true }]
  );

  async function onSubmit(formData) {
    const text = formData.get('text');
    addOptimistic({ id: 'temp-' + Date.now(), text });
    await sendMessage(text);   // when this resolves, `messages` updates,
                               // the optimistic copy is discarded
  }

  return (
    <>
      <ul>
        {optimistic.map(m => (
          <li key={m.id} className={m.sending ? 'sending' : ''}>{m.text}</li>
        ))}
      </ul>
      <form action={onSubmit}>
        <input name="text" required />
        <button>Send</button>
      </form>
    </>
  );
}

The two rules that matter

  1. Call addOptimistic inside an action or transition. The optimistic value lives only as long as that transition; called outside one, it has nothing to be reconciled against and React warns you.
  2. Keep the temp id stable. The list key must not change when the server assigns the real id, or React remounts the row and you lose the nice "fades from pending to confirmed" transition.

What optimism costs

You are betting the server says yes. For a "like" that is a fine bet; for a payment it is not. The pattern to keep is: optimistic for the cheap, reversible, high-frequency actions, and a real pending state (part 13) for anything where a wrong-looking UI is worse than a half-second wait. Failures are still handled the honest way — the action throws or returns an error, the real state does not change, and the optimistic row simply disappears.

Next: React 19's Actions and useActionState, which turn the whole submit-validate-pending-error loop into a first-class form lifecycle.