webdev.complete
๐Ÿš€ React 19 Superpowers
โš›๏ธReact
Lesson 89 of 117
25 min

useOptimistic & use()

Instant UI before the server confirms. Promise unwrapping.

Two more React 19 hooks worth knowing about: useOptimistic and use. The first makes "like" buttons feel instant. The second is a strange little hook that can unwrap promises and read contexts conditionally. Both are quietly transformative.

useOptimistic: feel the update before the server agrees

The classic problem: user clicks "Like." You fire a request to the server. The button doesn't look pressed until the response comes back. Even on a fast network that's 100 ms of perceived lag. Optimistic UI fixes it by updating the screen immediately and rolling back only if the request fails.

tsx
import { useOptimistic, useState, useTransition } from "react";

function Likes({ initial }) {
  const [likes, setLikes] = useState(initial);
  const [optimisticLikes, addOptimistic] = useOptimistic(
    likes,
    (current, delta) => current + delta
  );
  const [, startTransition] = useTransition();

  async function handleLike() {
    startTransition(async () => {
      addOptimistic(1);            // UI updates immediately
      const next = await api.like(); // round-trip
      setLikes(next);              // real state catches up
    });
  }

  return <button onClick={handleLike}>โ™ฅ {optimisticLikes}</button>;
}

useOptimistic(realValue, reducer)returns a "pretend value" that you can update synchronously. After the transition completes, React automatically reverts the optimistic value and uses whatever the real state is now. If the action fails and you don't update real state, the optimistic value vanishes and the UI snaps back.

Optimistic โ‰  permanent
The optimistic update is per-transition. It's a temporary lie layered on top of the real value. When the transition ends, React throws it away. You must update real state in the action to make it stick.

use(promise): unwrap async values in Suspense

The usehook is React's answer to "just give me the value, I don't want to write isLoading checks." Pass it a promise, and the component suspends until the promise resolves. Pass it a context, and you get the context value. The trick: use can be called conditionally. Unlike every other hook.

tsx
import { use, Suspense } from "react";

function UserName({ userPromise }) {
  const user = use(userPromise); // suspends here until promise resolves
  return <h1>Hello, {user.name}</h1>;
}

function App() {
  const userPromise = fetchUser(); // create the promise
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserName userPromise={userPromise} />
    </Suspense>
  );
}

While the promise is pending, React shows the nearest Suspense fallback. When it resolves, the component renders with the value. No useEffect, no loading state, no error checks (use ErrorBoundary for those).

Create promises in a stable place
Don't create the promise inside the component body without memoization. Every render creates a new promise, every promise suspends, you get an infinite loop. In server components or with TanStack Query, this is handled for you. In client-only code, use a framework or a cache.

use(context): the conditional context read

useContext can only be called unconditionally at the top of a component. use is the exception: you can call it inside an if, a loop, or anywhere else.

tsx
function Greeting({ showName }) {
  if (showName) {
    const user = use(UserContext); // legal!
    return <h1>Hi {user.name}</h1>;
  }
  return <h1>Hi friend</h1>;
}

This sounds small but it's big. Component code that reads context only sometimes used to require restructuring. Now you just write the conditional and call use.

Try it: optimistic like button

Click the heart. Notice it ticks up instantly, even though the fake server takes 800 ms. Try clicking rapidly; the UI batches optimistic updates while the real count catches up.

import { useOptimistic, useState, useTransition } from "react";

async function fakeApiLike(count) {
  await new Promise((r) => setTimeout(r, 800));
  // 1 in 5 fail to demo rollback
  if (Math.random() < 0.2) throw new Error("Network error");
  return count + 1;
}

export default function App() {
  const [likes, setLikes] = useState(0);
  const [error, setError] = useState(null);
  const [optimisticLikes, addOptimistic] = useOptimistic(
    likes,
    (current, delta) => current + delta
  );
  const [isPending, startTransition] = useTransition();

  async function handleLike() {
    setError(null);
    startTransition(async () => {
      addOptimistic(1);
      try {
        const next = await fakeApiLike(likes);
        setLikes(next);
      } catch (e) {
        setError(e.message);
        // Real state didn&apos;t update; optimistic value reverts
      }
    });
  }

  return (
    <div style={{ padding: 32, fontFamily: "system-ui", textAlign: "center" }}>
      <h2>useOptimistic demo</h2>
      <p style={{ fontSize: 48, margin: "16px 0" }}>
        <button
          onClick={handleLike}
          style={{ fontSize: 32, padding: "12px 16px", cursor: "pointer" }}
        >
          {"โ™ฅ"} {optimisticLikes}
        </button>
      </p>
      <p style={{ fontSize: 12, color: "#6b7280" }}>
        Real likes (server-confirmed): {likes}
        {isPending && " ยท syncing..."}
      </p>
      {error && (
        <p style={{ color: "#dc2626" }}>
          {error} (the heart snaps back to confirmed value)
        </p>
      )}
      <p style={{ fontSize: 11, color: "#9ca3af", marginTop: 24 }}>
        Server takes 800ms and fails 20% of the time. Click rapidly to see
        optimistic batching.
      </p>
    </div>
  );
}

Quiz

Quiz1 / 3

What happens to the optimistic value if your transition fails?

Recap

  • useOptimistic lets you show a hypothetical state during an async action. Reverts when the transition ends if not committed.
  • Pair it with useTransition + a real state setter to make the optimistic update permanent on success.
  • use(promise) suspends until resolved. The nearest Suspense shows its fallback.
  • use(context) can be called conditionally, unlike useContext.
  • Optimistic UI is one of the highest-impact UX upgrades. Apply it generously to actions where the success path is the common case.