ProgramGuru

useOptimistic: Temporary Feedback While a Save Is Pending

useOptimistic: Temporary Feedback While a Save Is Pending

A teacher marks attendance and expects quick feedback, but the real save may take time or fail. A temporary displayed change should not be confused with confirmed saved data.

useOptimistic displays a temporary optimistic value while an Action is pending. The underlying confirmed state remains the source of truth. An optimistic change anticipates success; it must reconcile with the actual result rather than promise success.

  1. Understand

    Separate confirmed and temporary values.

  2. Build

    Apply an optimistic update in an Action context.

  3. Check

    Describe failure and reconciliation honestly.

A temporary view follows an Action, not a confirmed save

  1. Confirmed

    Attendance begins absent.

  2. Optimistic

    Show present while the local operation is pending.

  3. Reconcile

    Commit confirmed success or return to the confirmed value.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

Temporary state is not a second permanent truth

useOptimistic takes a base value and optionally an update function describing how optimistic input changes the displayed value. The Hook returns the optimistic value and an update dispatcher. The optimistic update must happen within an Action or Transition rather than being treated as an ordinary lasting setState.

When the Action completes, the optimistic value is reconciled against the current base state. If the confirmed value was updated to present, the displayed present value remains. If the operation failed and the base stayed absent, the temporary change does not become a confirmed fact. Explain what happened to users rather than silently implying persistence.

Mark attendance optimistically with a local success fixture

The local operation waits briefly and always succeeds. It saves nothing to a server. While pending, the optimistic view can say Present with a pending marker; after completion the confirmed state becomes present. React scheduling may make a short pending state difficult to observe, so the final result is the stable check.

React · JavaScript with JSX

import { useOptimistic, useState, useTransition, startTransition } from 'react';
export default function App() {
  const [present, setPresent] = useState(false);
  const [optimistic, setOptimistic] = useOptimistic(present);
  const [pending, run] = useTransition();
  function mark() {
    run(async () => {
      setOptimistic(true);
      await new Promise(resolve => setTimeout(resolve, 60));
      startTransition(() => setPresent(true));
    });
  }
  return <main><h1>Local attendance preview</h1><p>{optimistic ? 'Present' : 'Absent'}{pending ? ' (pending)' : ''}</p>
    <button disabled={pending || present} onClick={mark}>Mark present</button>
    <p>Confirmed: {present ? 'present' : 'absent'}</p></main>;
}

Expected screen and behavior

Mark present temporarily displays Present while pending. The local successful operation then makes Confirmed: present. No real attendance is stored.

Follow temporary and confirmed values separately

React · Mark attendance optimistically with a local success fixture

import { useOptimistic, useState, useTransition, startTransition } from 'react';
export default function App() {
  const [present, setPresent] = useState(false);
  const [optimistic, setOptimistic] = useOptimistic(present);
  const [pending, run] = useTransition();
  function mark() {
    run(async () => {
      setOptimistic(true);
      await new Promise(resolve => setTimeout(resolve, 60));
      startTransition(() => setPresent(true));
    });
  }
  return <main><h1>Local attendance preview</h1><p>{optimistic ? 'Present' : 'Absent'}{pending ? ' (pending)' : ''}</p>
    <button disabled={pending || present} onClick={mark}>Mark present</button>
    <p>Confirmed: {present ? 'present' : 'absent'}</p></main>;
}

Initially the base is false, so both the optimistic view and the confirmed label say absent. The Action applies a temporary true value. The confirmed label still reads the separate base state until the successful fixture completes.

The post-await base update is explicitly wrapped in startTransition. After the Action settles, the optimistic display follows the updated base. This example follows the documented handling of post-await state updates; do not assume an async boundary automatically labels every later update as a Transition.

Choose whether optimism is suitable for the operation

An easily reversible attendance display may support optimistic feedback. A payment, irreversible permission change or action with a high likelihood of rejection may need a more cautious pending state. A fast interface is useful only if users can distinguish what is confirmed.

For lists, an optimistic temporary entry needs a stable temporary identity and later reconciliation with its confirmed server identity. Overlapping Actions and responses require the data layer’s supported ordering and conflict rules; a local success-only fixture does not implement that design.

Try the school task

React · starting example

import { useOptimistic, useState, useTransition, startTransition } from 'react';
export default function App() {
  const [present, setPresent] = useState(false);
  const [optimistic, setOptimistic] = useOptimistic(present);
  const [pending, run] = useTransition();
  function mark() {
    run(async () => {
      setOptimistic(true);
      await new Promise(resolve => setTimeout(resolve, 60));
      startTransition(() => setPresent(true));
    });
  }
  return <main><h1>Local attendance preview</h1><p>{optimistic ? 'Present' : 'Absent'}{pending ? ' (pending)' : ''}</p>
    <button disabled={pending || present} onClick={mark}>Mark present</button>
    <p>Confirmed: {present ? 'present' : 'absent'}</p></main>;
}

Make the local operation fail and preserve the confirmed absent value. Display a clear reason so the temporary present preview is not mistaken for success.

Worked answer

The Action catches the local rejection, leaves the base false and stores a failure message. When pending work ends, the optimistic display follows the unchanged base. This is a reproducible local failure, not a real server’s policy.

React · JavaScript with JSX

import { useOptimistic, useState, useTransition } from 'react';
export default function App() {
  const [confirmed] = useState(false);
  const [optimistic, setOptimistic] = useOptimistic(confirmed);
  const [error, setError] = useState('');
  const [pending, run] = useTransition();
  function mark() {
    run(async () => {
      setOptimistic(true);
      try { await new Promise((resolve, reject) => setTimeout(() => reject(new Error('Local save failed')), 60)); }
      catch { setError('Not saved; attendance remains absent'); }
    });
  }
  return <main><h1>Optimistic failure fixture</h1><p>{optimistic ? 'Present' : 'Absent'}</p>
    <button disabled={pending} onClick={mark}>Try mark present</button><p role="alert">{error}</p></main>;
}

Expected screen and behavior

After the local failure, Absent is restored and Not saved; attendance remains absent explains the result.

Mistakes and boundaries

React 19 or later is required. useOptimistic is not persistent storage, request cancellation or server conflict resolution.

Handle expected failure with an appropriate result and UI. An optimistic indicator must not bypass authorization or claim that a transaction has committed.

Keep the optimistic update function pure if one is supplied. Do not send a network request from a reducer-like calculation used to derive the temporary view.

Check your reasoning

React · Mark attendance optimistically with a local success fixture

import { useOptimistic, useState, useTransition, startTransition } from 'react';
export default function App() {
  const [present, setPresent] = useState(false);
  const [optimistic, setOptimistic] = useOptimistic(present);
  const [pending, run] = useTransition();
  function mark() {
    run(async () => {
      setOptimistic(true);
      await new Promise(resolve => setTimeout(resolve, 60));
      startTransition(() => setPresent(true));
    });
  }
  return <main><h1>Local attendance preview</h1><p>{optimistic ? 'Present' : 'Absent'}{pending ? ' (pending)' : ''}</p>
    <button disabled={pending || present} onClick={mark}>Mark present</button>
    <p>Confirmed: {present ? 'present' : 'absent'}</p></main>;
}

Choose the answer that follows the data and the interaction, then read the explanation for each alternative.

The Action fails and confirmed attendance remains absent. What should the interface communicate?
Choose one answer. You can try again.

Answer and explanation

The failure and the unchanged confirmed absence.

Correct. Users need to distinguish anticipation from confirmed data.

Carry the idea forward

Use optimistic feedback only with clear reconciliation and failure behavior. Review Action scheduling and mutation and cache policies.

Read the primary reference: React: useOptimistic.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Server and Client Components: Understand the Boundary →