cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Optimistic updates

Make React apps feel instant with useOptimistic: show a like or a chat message before the server answers, and roll back by itself when a save fails.

By the end, you can
  • 01
    Show a result before the server answersUse useOptimistic inside an action, from startTransition or a form's action, so a click shows its result at once.
  • 02
    Explain where the shown value comes fromTell the real state from the optimistic value, and say why the screen goes back by itself when a save fails.
  • 03
    Choose when to be optimisticAdd to a list with a reducer so overlapping sends work, and wait for the server when a wrong guess would cost money or trust.

A like button that makes you wait

When you like a photo, the app has to tell its server, and the server takes time to answer. Here is a like button that does the simple thing: it asks the server and waits. The server is pretend: the api.js tab holds a saveLike function that answers after one second. Click Like in the result.

A like button that waits for the serverPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { saveLike } from "./api.js"; export default function App() {  const [liked, setLiked] = useState(false);  const [saving, setSaving] = useState(false);   async function handleClick() {    setSaving(true);    const saved = await saveLike(!liked);    setLiked(saved);    setSaving(false);  }   return (    <>      <button onClick={handleClick} disabled={saving}>        {saving ? "Saving…" : liked ? "Liked" : "Like"}      </button>      <p>{liked ? 13 : 12} likes</p>    </>  );}
ResultLoading…

Line 5 keeps whether you liked the post, and line 6 keeps whether a save is running. handleClick (lines 8 to 13) sets saving on line 9, then waits for the server on line 10 with await. Line 11 stores the server's answer, and line 12 ends the wait. While saving is true, line 18 shows Saving… and line 17 turns the button off.

For one whole second, the button says Saving… and the count stays at 12. On a phone with a weak signal that can be several seconds. Yet the server says yes to almost every like, so the wait shows the person nothing they didn't already know.

An optimistic update shows the result before the server confirms it, on the bet that it will. React 19 added a hook for it, useOptimistic (announced in the React 19 release post). Here is the same button with it, talking to the same slow server:

The same button, optimisticPop out in the code editor (opens in a new tab)JSX
import { startTransition, useOptimistic, useState } from "react";import { saveLike } from "./api.js"; export default function App() {  const [liked, setLiked] = useState(false);  const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);   function handleClick() {    startTransition(async () => {      setOptimisticLiked(!optimisticLiked);      const saved = await saveLike(!optimisticLiked);      startTransition(() => setLiked(saved));    });  }   return (    <>      <button onClick={handleClick}>{optimisticLiked ? "Liked" : "Like"}</button>      <p>{optimisticLiked ? 13 : 12} likes</p>    </>  );}
ResultLoading…

Click Like: the button says Liked and the count says 13 likes at once. The server still takes a second, but nothing on the screen waits for it. Click again to unlike, and that's instant too. The next section explains every line.

Two timelines for one click on Like. Top, waiting: click, then Saving for one second, then Liked. Bottom, optimistic: click, then Liked right away for the whole second, then Liked when the server answers.Waiting for the serverclickSaving…Liked0 snothing new for 1 second1 sOptimisticclickLikedLiked0 sshown at once1 s: saved
Both buttons wait one second for the same server. The top one shows Saving… until it answers; the bottom one shows Liked at once and keeps showing it.
Real-life analogyAn order board at a café

At a busy café, the cashier chalks your order on the board the moment you ask, so you can see it's on its way. The kitchen keeps its own order book. When the kitchen is done with your order, the board is copied from the book again: your tea stays on the board if they made it, and it's gone if they were out of tea.

In real life: The cashier chalks your order on the board as soon as you ask
In React: setOptimisticLiked(true) shows the like at once
In real life: The kitchen checks that it can make your tea
In React: The action waits for the server
In real life: The kitchen's order book
In React: The real state, liked, which changes only when the server says yes
In real life: When the kitchen answers, the board is copied from the book again
In React: When the action ends, the screen shows the real state again
The main idea

An optimistic update shows the result of an action before the server confirms it. With useOptimistic, you set a temporary value inside an action. React shows it while the action runs, and when the action ends, it shows your real state again: the saved result, or the old value if the save failed.

useOptimistic, line by line

The hook takes one value and gives back two things, like useState:

The shape of useOptimisticJavaScript
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
  • value is the real value, usually a piece of state or a prop. While no action is running, the hook gives it back as it is.
  • optimisticValue is the value to show. It equals value, except while an action is running and you have set it.
  • setOptimisticValue sets that temporary value. You can only call it inside an action.

An action is a function React runs inside a transition: a group of updates React treats as one piece of work, and keeps track of until it finishes. You get one in two ways. You pass a function to startTransition, which you import from react, or you give a function to a form's action prop, which React starts in a transition for you, as Form actions shows. An async function counts: the action lasts until its last await is done. Transitions get a full lesson of their own later, Transitions.

Here is the optimistic like button again, so you can follow it line by line:

The optimistic like button, againPop out in the code editor (opens in a new tab)JSX
import { startTransition, useOptimistic, useState } from "react";import { saveLike } from "./api.js"; export default function App() {  const [liked, setLiked] = useState(false);  const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);   function handleClick() {    startTransition(async () => {      setOptimisticLiked(!optimisticLiked);      const saved = await saveLike(!optimisticLiked);      startTransition(() => setLiked(saved));    });  }   return (    <>      <button onClick={handleClick}>{optimisticLiked ? "Liked" : "Like"}</button>      <p>{optimisticLiked ? 13 : 12} likes</p>    </>  );}
ResultLoading…
  1. Line 5 is the real state, liked. Only the server's answer changes it.
  2. Line 6 passes liked to useOptimistic and gets optimisticLiked and its setter. Lines 18 and 19 show optimisticLiked, never liked.
  3. Line 9 starts an action: startTransition gets an async function and runs it right away.
  4. Line 10 sets the temporary value. React renders at once with optimisticLiked as true, so the button says Liked.
  5. Line 11 waits one second for the server, which answers with the value it saved.
  6. Line 12 puts the answer in the real state, inside another startTransition.

Why a second startTransition on line 12? Inside an async function, React can't yet tell that the code after an await still belongs to the action: JavaScript gives React no way to follow it across the wait. So React's docs ask you to wrap state updates after an await in startTransition, which keeps them part of the action. The chat later in this lesson shows what goes wrong without it.

When the async function finishes, the action ends, and React renders with the real state. liked is true by then, so optimisticLiked is true too, and nothing on the screen moves.

Read it in React's own code

useOptimistic lives in React's ReactFiberHooks.js, at release v19.3.1, the version the live examples run. Your setter is dispatchOptimisticSetState: it shows your value right away and marks it to be removed when the transition ends. On every render, updateOptimisticImpl starts again from the value you passed in, shown below.

updateOptimisticImpl, from ReactFiberHooks.js (v19.3.1, lines 2013 to 2032)JavaScript
function updateOptimisticImpl<S, A>(
  hook: Hook,
  current: Hook | null,
  passthrough: S,
  reducer: ?(S, A) => S,
): [S, (A) => void] {
  // Optimistic updates are always rebased on top of the latest value passed in
  // as an argument. It's called a passthrough because if there are no pending
  // updates, it will be returned as-is.
  //
  // Reset the base state to the passthrough. Future updates will be applied
  // on top of this.
  hook.baseState = passthrough;

  // If a reducer is not provided, default to the same one used by useState.
  const resolvedReducer: (S, A) => S =
    typeof reducer === 'function' ? reducer : (basicStateReducer as any);

  return updateReducerImpl(hook, currentHook as any as Hook, resolvedReducer);
}

Line 13 resets the starting point to passthrough, the value you gave the hook. Line 19 then applies the optimistic updates that are still waiting on top of it, with your reducer, or with the same one useState uses (lines 16 and 17).

The setter must run inside an action. Here is the same button with handleClick written as a plain async function, a mistake on purpose. Click Like:

The setter outside an action (a mistake on purpose)Pop out in the code editor (opens in a new tab)JSX
import { startTransition, useOptimistic, useState } from "react";import { saveLike } from "./api.js"; export default function App() {  const [liked, setLiked] = useState(false);  const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);   async function handleClick() {    setOptimisticLiked(!optimisticLiked);    const saved = await saveLike(!optimisticLiked);    startTransition(() => setLiked(saved));  }   return (    <>      <button onClick={handleClick}>{optimisticLiked ? "Liked" : "Like"}</button>      <p>{optimisticLiked ? 13 : 12} likes</p>    </>  );}
ResultLoading…

Nothing changes for a second, then the button says Liked. Line 9 runs in a plain click handler, not in an action, so React has no action to keep the value for: it renders true and goes straight back to liked, too fast to see. The like only shows when line 11 sets the real state. The console under the result shows React's warning:

React's warning in the consoleText
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.

It means the setter ran when no action was running. Press Edit & run and wrap lines 9 to 11 in startTransition(async () => { … }), as in the button above, and the like shows at once again.

Two values: the real one and the one on screen

While the action runs, the component has two values. liked is what the server has saved, as far as the app knows. optimisticLiked is what the screen shows. They differ only while the action runs:

A table of cards. Columns: before the click, while saving, after the save. Row liked, the real state: false, false, true. Row optimisticLiked, what the screen shows: false, true, true.before the clickfalsefalsewhile savingfalsetrueafter the savetruetruelikedthe real stateoptimisticLikedon the screen
liked changes once, when the server answers. optimisticLiked is true from the click on. The two only differ while the action runs.

React works out optimisticLiked on every render. If no action is running, it's liked. If an action is running and you've set it, it's your value. Nothing clears the temporary value: when the action ends, React simply stops using it. And because setLiked(saved) on line 12 is part of the same action, the render after the action already has the new liked, so the screen never flicks back to Like on the way.

Watch it happen. This post runs the like button with React's real useOptimistic, and the table lists both values after every render. Press Like, wait a second, then choose Fails and press it again.

Playground: the real value and the shown value
The postserver saves

Asha's new tea shop photo

12 likes
Every render0 renders
WhenlikedoptimisticLiked
Try it yourself
The server:

Press Like in the post. Each row of the table is one render React put on the screen, with both values at that moment.

The post runs the like button above with React's real useOptimistic, and the server answers after 1 second. Each row is written by a layout effect, right after React commits a render. One like per round: choosing an answer or pressing Reset starts over.

With Saves the like, the table has three rows: false, false at first, false, true right after the click, and true, true one second later. With Fails, the last row is false, false: the action ended, liked never changed, and the screen went back to it.

How to read the table: each row is one moment in the like button's life, and the screen always shows the last column.

What each value is, moment by moment
When?What is liked?What does the screen show?
Before any clickfalse.false: no action is running, so it's liked.
Right after the clickStill false.true: an action is running and you set it.
The server saved the liketrue, set on line 12.true: the action ended, so it's liked again, which is now true.
The server failedStill false: nothing set it.false: the action ended, so it's liked again, which never changed.

This also tells you when a save is running: optimisticLiked !== liked is true only during an action. You could use it to fade the button a little while the server works.

When the server says no

Servers fail. The network drops, a server is down, or a rule says no. This like button has an Offline box that makes the pretend server fail after its one-second wait. Tick Offline, then click Like.

A like that can failPop out in the code editor (opens in a new tab)JSX
import { startTransition, useOptimistic, useState } from "react";import { saveLike } from "./api.js"; export default function App() {  const [liked, setLiked] = useState(false);  const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);  const [offline, setOffline] = useState(false);  const [error, setError] = useState("");   function handleClick() {    setError("");    startTransition(async () => {      setOptimisticLiked(!optimisticLiked);      try {        const saved = await saveLike(!optimisticLiked, offline);        startTransition(() => setLiked(saved));      } catch (e) {        setError(e.message);      }    });  }   return (    <>      <label>        <input type="checkbox" checked={offline} onChange={(e) => setOffline(e.target.checked)} /> Offline      </label>{" "}      <button onClick={handleClick}>{optimisticLiked ? "Liked" : "Like"}</button>      <p>{optimisticLiked ? 13 : 12} likes</p>      {error && <p role="alert">{error}</p>}    </>  );}
ResultLoading…

The button says Liked for one second, then Like again, the count goes back to 12, and a message explains why. Line 7 keeps the Offline box, and line 15 passes it to saveLike, which throws an error when it's true (line 4 of api.js). The try and catch on lines 14 to 19 catches it: line 16 never runs, so liked stays false, and line 18 keeps the message, which line 30 shows. Line 11 clears an old message when you try again.

Look for the line that puts Like back: there isn't one. The action ended, React rendered with the real state, and the real state never changed. This going back is called a rollback, and with useOptimistic you get it for free, because the optimistic value only lasts as long as the action.

While saving, the screen shows Liked. An arrow up leads to the server saves: liked true, the screen shows Liked. An arrow down leads to the server fails: liked false, the screen shows Like, because liked never changed.while savingscreen: Likedthe server savesliked: truescreen: Likedthe server failsliked: falsescreen: Likeliked never changed: nothing to undo
The action ends either way. If the server saved the like, setLiked made liked true. If it failed, liked is still false, so the screen goes back to it, with no code to undo the like.

Catch the error anyway. Without the try, the like still goes back when the action ends, but nothing tells the person why, and the error shows up in the browser's console as an uncaught error. A short message next to the button, as line 30 shows, tells them the like didn't stick.

Adding to a list with a reducer

In a chat, a message should appear the moment you press Send, marked as sending until the server has it. The optimistic value is now a list, and each send adds one message to it. For that, useOptimistic takes a second argument: a reducer, a pure function like the ones in Extracting state logic into a reducer. It gets the current list and whatever you pass to the setter, and returns the list to show. Send a few messages; the pretend server fails every third one.

A chat that shows your message at oncePop out in the code editor (opens in a new tab)JSX
import { startTransition, useOptimistic, useState } from "react";import { sendMessage } from "./api.js"; function addPending(messages, newMessage) {  return [...messages, { ...newMessage, sending: true }];} export default function App() {  const [messages, setMessages] = useState([{ id: "m1", text: "Is the shop open?" }]);  const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages, addPending);  const [error, setError] = useState("");   async function send(formData) {    const newMessage = { id: crypto.randomUUID(), text: formData.get("text") };    addOptimisticMessage(newMessage);    try {      const saved = await sendMessage(newMessage);      startTransition(() => {        setMessages((list) => [...list, saved]);        setError("");      });    } catch (e) {      setError(e.message);    }  }   return (    <form action={send}>      <ul>        {optimisticMessages.map((message) => (          <li key={message.id}>            {message.text} {message.sending && <small>(sending…)</small>}          </li>        ))}      </ul>      <input name="text" aria-label="Message" required /> <button>Send</button>      {error && <p role="alert">{error}</p>}    </form>  );}
ResultLoading…
  1. Lines 4 to 6 are the reducer, addPending. It returns a new list with the new message at the end, marked sending: true, using spread twice.
  2. Line 10 passes the real messages and addPending. The setter takes one message, so it's named addOptimisticMessage.
  3. Line 13, send, is the form's action (line 28). React already runs it in a transition, so line 15 needs no startTransition.
  4. Line 14 makes the message with an id from crypto.randomUUID(), which returns a random id string. The server sends the same message back, so the key on line 31 stays the same when it's saved.
  5. Line 15 adds it to the screen at once. Line 17 waits for the server, and lines 18 to 21 add the saved message to the real list and clear an old error, in a transition.
  6. Line 32 shows (sending…) only on a message that addPending marked.

The third message you send shows (sending…) for a second, then disappears, and the error appears: the same rollback as the like. The form empties after each send, as Form actions explains, so type the failed message again to retry.

A crate named messages holds one card: Is the shop open? A tag reads pending: Tea at 4?. Arrows from both lead to a function card, addPending. An arrow leads to a crate named on screen, holding Is the shop open? and Tea at 4? (sending…).Is the shop open?messagespending:Tea at 4?addPendingIs the shop open?Tea at 4? (sending…)on screen
On every render while the action runs, React passes the newest messages and each pending message to addPending. The screen shows what it returns.

Why a reducer instead of setting the whole list? React runs addPending on every render while the action runs, each time with the newest messages. If the real list changes during the wait, say a friend's reply arrives, your pending message is added to the new list, not to an old copy. And each call to the setter adds one message to a queue, so two sends that overlap both show. Try three ways of writing the chat, each sending two messages 0.3 seconds apart:

Playground: two messages, 0.3 seconds apart
The lines that differ (A plain value)JavaScript
const [optimisticMessages, setOptimisticMessages] = useOptimistic(messages);

// in send(text), inside startTransition(async () => { … }):
setOptimisticMessages([...messages, { ...newMessage, sending: true }]);
const saved = await sendMessage(newMessage);
startTransition(() => setMessages((list) => [...list, saved]));
The chatA plain value
  • Is the shop open?
What the screen showed0 renders
    Try it yourself
    Pick a way, then send

    Press Send two messages. "Tea at 4?" goes at once and "And cake?" 0.3 seconds later; the server answers each one 1 second after it was sent.

    The chat runs React's real useOptimistic with the three lines shown for each way; the rest is the chat from the reducer example above, with a server that never fails. Each row of the timeline is written right after React commits a render. Choosing a way or pressing Reset starts over.

    With A plain value, the second send replaces the whole optimistic list, built from messages, which doesn't have the first message yet. Tea at 4? disappears while it's still on its way. With A reducer, both stay. With A reducer, no second startTransition, the saved Tea at 4? shows at 1 second next to its pending copy: the message shows twice. That's why line 18 wraps setMessages in startTransition.

    Notice one more thing in every timeline: Tea at 4? stays (sending…) until 1.3 seconds, though its answer came at 1 second. While actions overlap, React waits until all of them have finished, then shows their results in one render.

    How to read the table: each row is one way to set the optimistic value, from the simplest to the most flexible.

    Three ways to set an optimistic value
    How you call itWhat React showsGood for
    setOptimisticLiked(true), a plain valueExactly that value, until the action ends.One value, like a like, a star, or a name.
    setOptimisticLikes((n) => n + 1), an updater functionWhat your function returns. React runs it again on every render, on the newest real value plus the earlier pending updates.A value that changes from its last value, like a count.
    addOptimisticMessage(newMessage), with a reducerWhat the reducer returns from the newest real value and each pending message, on every render.Adding to or changing a list, or several kinds of change in one place.

    A note on the updater row. React's useOptimistic reference (checked in October 2026) says an updater function only sees the value from when the action started. In React 19.3.1, the version these examples run, an updater builds on the newest value, the same as a reducer. React keeps each updater in the hook's queue, just as it keeps each message you pass to a reducer, and on every render it runs the whole queue again, starting from the newest real value: that's what updateOptimisticImpl, shown earlier, does. So if a friend's reply arrives while your message is sending, an updater keeps the reply on the screen too. A reducer is still the clearer choice for a list, because each setter call only has to say what changed.

    When to be optimistic, and when to wait

    An optimistic update is a guess. It pays off when the guess is almost always right and a wrong guess is cheap: the person sees the like disappear and a message, and taps again. It hurts when a wrong guess costs money or trust: a payment shown as done that then fails, or a train seat shown as booked that someone else got first.

    How to read the table: ask each question about the action you're building.

    Questions to ask before you show a result early
    AskIf yesFor example
    Does the server say yes almost every time?Show the result at once.Likes, stars, and chat messages nearly always save.
    Would a wrong guess cost money or trust?Wait, and show that you're waiting.A payment shown as done that then fails.
    Does only the server know the result?Wait for its answer.A coupon's discount, or which seat you got.
    Is it hard to undo?Wait, and ask first.Deleting an account.

    When you decide to wait, show that something is happening instead: a pending state, like the isPending from useActionState in Form actions, and a button that can't be pressed twice.

    Show it right away, or wait?
    • Liking a friend's photo
    • Paying for an order
    • Sending a chat message
    • Booking the last seat on a train
    • Renaming a to-do
    • Deleting your account
    • Starring a recipe
    • Applying a coupon code that only the server can check
    Try it yourself
    0 of 8 correct

    Sort each action: would you show its result before the server answers, or wait for the server?

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    Common mix-ups

    • "useOptimistic saves to the server." No. It only changes what the screen shows. Your action still calls the server and sets the real state.
    • "When the save fails, I have to put the old value back." No. When the action ends, React shows the real state again, and the real state never changed.
    • "The optimistic value stays after the action." No. It only lasts while the action runs. If you forget to set the real state, the screen goes back to the old value even after a successful save.
    • "I can call the setter in any click handler." No. Only inside an action. Anywhere else React warns, and the value shows for an instant at most.
    • "A plain value works for lists too." Only while sends never overlap. Each call replaces the whole list, so a second send can hide the first. A reducer adds to the newest list.
    • "Optimistic is always the better choice." No. For payments, bookings, deleting an account, or a result only the server knows, wait and show that you're waiting.

    Practice exercises

    Exercise 1 · Warm-upPredict the count while saving

    You click Like once. What does the page say while the server is still saving? Answer first, then press Edit & run to check.

    import { startTransition, useOptimistic, useState } from "react";
    import { saveLike } from "./api.js";
    
    export default function App() {
      const [likes, setLikes] = useState(12);
      const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
    
      function handleClick() {
        startTransition(async () => {
          setOptimisticLikes(likes + 1);
          await saveLike(true);
          startTransition(() => setLikes(likes + 1));
        });
      }
    
      return (
        <>
          <button onClick={handleClick}>Like</button>
          <p>{optimisticLikes} likes</p>
        </>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 2 · Warm-upPredict the count after a failure

      The server in this example always fails. You click Like once and wait. What does the page say after the server has answered?

      import { startTransition, useOptimistic, useState } from "react";
      import { saveLike } from "./api.js";
      
      export default function App() {
        const [likes, setLikes] = useState(12);
        const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
      
        function handleClick() {
          startTransition(async () => {
            setOptimisticLikes(likes + 1);
            try {
              await saveLike(true, true);
              startTransition(() => setLikes(likes + 1));
            } catch {
              // The server failed. There's nothing to undo here.
            }
          });
        }
      
        return (
          <>
            <button onClick={handleClick}>Like</button>
            <p>{optimisticLikes} likes</p>
          </>
        );
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 3 · PracticeMake the Follow button optimistic

        This Follow button waits for the server. Change it so it says Following the moment you click, then type what the button says right after your click.

        import { useState } from "react";import { saveFollow } from "./api.js"; export default function App() {  const [following, setFollowing] = useState(false);  const [saving, setSaving] = useState(false);   async function handleClick() {    setSaving(true);    const saved = await saveFollow(!following);    setFollowing(saved);    setSaving(false);  }   return (    <>      <h3>Asha's Tea Room</h3>      <button onClick={handleClick} disabled={saving}>        {saving ? "Saving…" : following ? "Following" : "Follow"}      </button>    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 4 · PracticeFind the quantity that snaps back

          Click Add one twice, waiting for each save. The number jumps up and then back. What number does it show at the end? Then fix line 6.

          import { startTransition, useOptimistic, useState } from "react";import { saveQuantity } from "./api.js"; export default function App() {  const [quantity, setQuantity] = useState(3);  const [optimisticQuantity, setOptimisticQuantity] = useOptimistic(3);   function addOne() {    startTransition(async () => {      setOptimisticQuantity(quantity + 1);      const saved = await saveQuantity(quantity + 1);      startTransition(() => setQuantity(saved));    });  }   return (    <p>      Green tea × {optimisticQuantity} <button onClick={addOne}>Add one</button>    </p>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 5 · ChallengeWrite the reducer for a to-do list

            Adding a to-do shows nothing until the server answers, because addPending returns the list unchanged. Make it add the new to-do, marked as saving. Then add Bake cake and type what its line says while it saves.

            import { startTransition, useOptimistic, useState } from "react";import { saveTodo } from "./api.js"; function addPending(todos, newTodo) {  // Return a new list with newTodo at the end, marked as saving.  return todos;} export default function App() {  const [todos, setTodos] = useState([{ id: "t1", text: "Buy tea" }]);  const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, addPending);   async function add(formData) {    const newTodo = { id: crypto.randomUUID(), text: formData.get("text") };    addOptimisticTodo(newTodo);    const saved = await saveTodo(newTodo);    startTransition(() => setTodos((list) => [...list, saved]));  }   return (    <form action={add}>      <ul>        {optimisticTodos.map((todo) => (          <li key={todo.id}>            {todo.text} {todo.saving && <small>(saving…)</small>}          </li>        ))}      </ul>      <input name="text" aria-label="New to-do" required /> <button>Add</button>    </form>  );}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 6 · ChallengeRemove an address at once, and bring it back on failure

              The Remove buttons wait for the server. Make an address disappear the moment you press Remove. The server refuses to remove Home: make Home come back with the server's message.

              import { startTransition, useState } from "react";import { removeAddress } from "./api.js"; export default function App() {  const [addresses, setAddresses] = useState([    { id: "home", label: "Home: 4 Tea Street" },    { id: "work", label: "Work: 9 Cake Road" },  ]);   function remove(id) {    startTransition(async () => {      await removeAddress(id);      startTransition(() => setAddresses((list) => list.filter((address) => address.id !== id)));    });  }   return (    <ul>      {addresses.map((address) => (        <li key={address.id}>          {address.label} <button onClick={() => remove(address.id)}>Remove</button>        </li>      ))}    </ul>  );}
              ResultLoading…

                Check your understanding

                The optimistic updates quiz · 7 questionsScore: first tries count
                1. Question 1 of 7You click Like once. While the server is still busy, what does this page show?

                  Read the code, then predictJSX
                  const [likes, setLikes] = useState(12);
                  const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
                  
                  function handleClick() {
                    startTransition(async () => {
                      setOptimisticLikes(likes + 1);
                      await saveLike();
                      startTransition(() => setLikes(likes + 1));
                    });
                  }
                  // The page shows <p>{optimisticLikes} likes</p>

                  Choose an answer to see the explanation.

                2. Question 2 of 7This time the server answers with the total, 20, because others liked the post too. What does the page show once the action ends?

                  Read the code, then predictJSX
                  const [likes, setLikes] = useState(12);
                  const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
                  
                  function handleClick() {
                    startTransition(async () => {
                      setOptimisticLikes(likes + 1);
                      const total = await saveLike(); // the server answers 20
                      startTransition(() => setLikes(total));
                    });
                  }
                  // The page shows <p>{optimisticLikes} likes</p>

                  Choose an answer to see the explanation.

                3. Question 3 of 7Where can you call the setter that useOptimistic gives you?

                  Choose an answer to see the explanation.

                4. Question 4 of 7The server fails and your code catches the error. How does the screen go back to the old value?

                  Choose an answer to see the explanation.

                5. Question 5 of 7Why does the chat pass a reducer, addPending, to useOptimistic?

                  Choose an answer to see the explanation.

                6. Question 6 of 7Which of these should wait for the server instead of showing its result at once?

                  Choose an answer to see the explanation.

                7. Question 7 of 7After await sendMessage(…), the chat wraps setMessages in startTransition. What happens without it?

                  Choose an answer to see the explanation.

                Key takeaways

                • An optimistic update shows an action's result before the server confirms it.
                • useOptimistic(value) gives back value, except while an action runs and you've set a temporary value.
                • Call the setter inside an action: in startTransition, or in a form's action. Wrap state updates after an await in startTransition again.
                • Set the real state when the server says yes. When the action ends, React shows the real state.
                • A failed save rolls back by itself, because the real state never changed. Catch the error to say what happened.
                • Use a reducer to add to a list, so overlapping actions build on the newest list.
                • Wait instead when a wrong guess would cost money or trust, or when only the server knows the result.

                useOptimistic shows your guess while an action runs; when it ends, the real state wins.
                Set the guess inside an action, set the real state on success, and let React roll back the rest.

                Coming next: Referencing values with refs. It opens the next stage with values a component remembers without showing them.

                CompleteFrontend Clear concepts. Working examples.