React 19 Hooks: use(), useActionState & useOptimistic

Reviewed & published by Brayan K

React 19 ships a set of hooks built for the async, form-driven UIs you actually build. use() reads the value out of a promise (or context) and plugs into Suspense, so you stop hand-writing loading/error/data branches. useActionState manages a form action's returned state plus a pending flag in one hook. useFormStatus lets a child button know the surrounding form is submitting. And useOptimistic shows the expected result instantly, then reconciles with reality. Together they turn fiddly async glue into a few lines. By the end you'll know what each unwraps, when to reach for it, and how they pair with Server Actions.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ use(): Read Promises & Context

The use() hook unwraps a value. Pass it a promise and it returns the resolved data — suspending to the nearest <Suspense> while pending, and throwing to an error boundary if it rejects. The result: your component reads as if the data is simply there, with no loading/error branches in the body.

import { use, Suspense } from "react";

// The promise is created OUTSIDE render and passed in as a prop.
function UserName({ userPromise }) {
  const user = use(userPromise);     // suspends until it resolves
  return <h2>{user.name}</h2>;       // success path only — no isLoading branch
}

function Profile({ userPromise }) {
  return (
    <Suspense fallback={<p>Loading user…</p>}>
      <UserName userPromise={userPromise} />
    </Suspense>
  );
}

use() also reads context — and unlike every other hook, it can be called conditionally, inside an if or loop:

function Heading({ showThemed, children }) {
  if (showThemed) {
    const theme = use(ThemeContext);    // ✅ allowed — use() can be conditional
    return <h1 className={theme.heading}>{children}</h1>;
  }
  return <h1>{children}</h1>;
}

The demo models the three outcomes use() reacts to — pending suspends, fulfilled returns, rejected throws. Run it to lock in the mental model:

// The use() hook UNWRAPS a value: given a settled promise, it hands you the
// data (or throws to Suspense while pending / to an error boundary on reject).
// We can't run Suspense here, so we model the three outcomes use() reacts to.

function describeUse(promiseState) {
  switch (promiseState.status) {
    case "pending":
      // use() "suspends": React shows the nearest <Suspense fallback>
      return "⏳ suspend -> show <Suspense fallback={<Spinner/>} />";
    case "fulfilled":
      // use() returns the resolved value to your component
      return "✅ return value: " + JSON.stringify(promiseState.value);
    case "rejected":
      // use() throws: the nearest error boundary catches it
      return "❌ throw -> nearest error boundary renders";
  }
}

console.log(describeUse({ status: "pending" }));
console.log(describeUse({ status: "fulfilled", value: { name: "Ada" } }));
console.log(describeUse({ status: "rejected" }));
// ⏳ suspend -> show <Suspense fallback={<Spinner/>} />
// ✅ return value: {"name":"Ada"}
// ❌ throw -> nearest error boundary renders

2️⃣ useActionState & useFormStatus: Forms Done Right

useActionState(action, initialState) returns [state, formAction, isPending]. Your action receives the previous state and the form data and returns the next state — perfect for validation errors or a success message. Wire formAction to the form and you get isPending for free.

"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";    // a Server Action

const initial = { error: null, message: null };

function Newsletter() {
  const [state, formAction, isPending] = useActionState(subscribe, initial);

  return (
    <form action={formAction}>
      <input name="email" type="email" placeholder="you@dev.io" />
      <button disabled={isPending}>
        {isPending ? "Subscribing…" : "Subscribe"}
      </button>
      {state.error && <p className="text-red-600">{state.error}</p>}
      {state.message && <p className="text-green-600">{state.message}</p>}
    </form>
  );
}

When the submit button lives in its own reusable component, it can't see the parent's isPending. That's what useFormStatus is for — a child inside the <form> reads the surrounding form's status directly:

"use client";
import { useFormStatus } from "react-dom";

// Reusable across any form — no props needed.
function SubmitButton({ label = "Submit" }) {
  const { pending } = useFormStatus();   // status of the nearest enclosing <form>
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Working…" : label}
    </button>
  );
}

Now model the heart of useActionState: the action function that takes the previous state plus form data and returns the next state. Fill in the blanks:

// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// useActionState(action, initialState) gives you [state, formAction, isPending].
// The "action" receives the PREVIOUS state + the form data, and RETURNS the
// next state. Let's model that action function for a login form.

async function loginAction(prevState, formData) {
  const email = formData.email ?? "";

  // 1) validate: if email is missing, return an error state
  if (!email) {
    return { ___: "Email is required" };   // 👉 error
  }

  // 2) success: return a fresh state object (this replaces prevState)
  return { ___: "Welcome back!" };          // 👉 message
}

async function run() {
  // initialState is {} ; React passes prev state as the first arg
  console.log(await loginAction({}, { email: "" }));
  console.log(await loginAction({}, { email: "ada@dev.io" }));
}
run();

// ✅ Expected output:
//    { error: 'Email is required' }
//    { message: 'Welcome back!' }

3️⃣ useOptimistic: Instant, Reconciled UI

Network calls take time, but users want feedback now. useOptimistic(state, updateFn) returns an optimisticState and an addOptimistic function. You show the expected result immediately, run the real action, and when it finishes React automatically drops the optimistic value and re-renders from the true state — so a failure just snaps back, no manual undo.

"use client";
import { useOptimistic, useState } from "react";
import { saveLike } from "./actions";

function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);          // real, confirmed state
  const [optimisticLikes, addOptimistic] = useOptimistic(
    likes,
    (current, delta) => current + delta                       // how to apply the guess
  );

  async function handleLike() {
    addOptimistic(1);                  // 1) UI jumps to likes + 1 instantly
    const confirmed = await saveLike(postId);  // 2) the real action runs
    setLikes(confirmed);               // 3) reconcile with the server's truth
  }

  return (
    <button onClick={handleLike}>❤️ {optimisticLikes}</button>
  );
}

The demo models the show-then-reconcile timeline: the UI jumps to the optimistic value, then settles on the server's confirmed value. Run it:

// useOptimistic shows the EXPECTED result instantly, then reconciles with the
// real result when the async action finishes. We model that timeline: the UI
// jumps to the optimistic value, then settles on the server's truth.

async function fakeServerLike(current) {
  // pretend the server confirms and returns the authoritative count
  await Promise.resolve();
  return current + 1;
}

async function likePost(realLikes) {
  // 1) OPTIMISTIC: assume success, update the UI immediately (+1)
  let displayed = realLikes + 1;
  console.log("instant UI (optimistic):", displayed);

  // 2) run the real action and get the source-of-truth value back
  const confirmed = await fakeServerLike(realLikes);

  // 3) RECONCILE: React drops the optimistic value, re-renders from real state
  displayed = confirmed;
  console.log("after server confirms:  ", displayed);
  return displayed;
}

likePost(41);
// instant UI (optimistic): 42
// after server confirms:   42

🎯 Your Turn: Pick the Right Hook

Each scenario needs exactly one of these React 19 hooks. Finish pickHook so it returns the right name for each case — the decision you make in real code.

// Map a need to the React 19 hook that solves it.
function pickHook(need) {
  switch (need) {
    case "read a promise's value with Suspense":
      return "use";
    case "form state + pending in the form component":
      return "useActionState";
    case "spinner in a shared submit button":
      return "useFormStatus";
    case "show the result instantly before the server confirms":
      return "useOptimistic";
    default:
      return "useState";
  }
}

const needs = [
  "read a promise's value with Suspense",
  "form state + pending in the form component",
  "spinner in a shared submit button",
  "show the result instantly before the server confirms",
];

for (const n of needs) {
  console.log(pickHook(n).padEnd(15), "<-", n);
}

Reorder Challenge

These lines of an optimistic like handler are scrambled. Put them in the correct order:

A.   const confirmed = await saveLike(postId);
B. async function handleLike() {
C.   setLikes(confirmed);
D.   addOptimistic(1);
E. }

Quick Recall

1) Why must the promise passed to use() be created outside render?

Creating it in the body makes a new promise every render, which Suspense can never settle — an infinite loading loop. Create it outside render (prop, cache, or a Server Component) and pass the stable promise in.

2) What three things does useActionState return?

[state, formAction, isPending] — the latest state your action returned, the action to put on <form action>, and a pending flag while it runs.

3) Your reusable <SubmitButton> needs to know the form is submitting. Which hook?

useFormStatus (from react-dom). A child of the form reads { pending } directly — no prop drilling.

4) With useOptimistic, what happens if the real action fails?

React discards the optimistic value and re-renders from the real state — so the UI snaps back automatically. You never write manual undo logic.

Common Errors (and the fix)

📋 Quick Reference

HookFromUse it to…
use(promise)reactRead a promise's value with Suspense
use(context)reactRead context — even conditionally
useActionStatereactForm state + pending in one hook
useFormStatusreact-domPending status inside a child of a form
useOptimisticreactShow the expected result instantly

Mini-Challenge: Validate in an Action

Finish signupAction so it returns an error state for bad input and a message state on success — exactly the shape useActionState threads back into your form.

🎉 Lesson Complete

Practice quiz

What does the use() hook do with a promise?

  • Starts a new fetch
  • Caches the result forever
  • Reads (unwraps) the promise's value, suspending while pending
  • Converts it to state

Answer: Reads (unwraps) the promise's value, suspending while pending. use() unwraps a promise: it suspends to the nearest <Suspense> while pending and returns the resolved value.

Why must the promise passed to use() be created outside render?

  • Creating it in the body makes a new promise every render, which Suspense can never settle
  • It runs faster
  • Promises can't be created in components
  • To avoid TypeScript errors

Answer: Creating it in the body makes a new promise every render, which Suspense can never settle. A new promise per render causes an infinite loading loop; create it outside render and pass a stable one.

Unlike other hooks, use() may be called…

  • Only at the top level
  • Only in class components
  • Only once per app
  • Conditionally, inside an if or a loop

Answer: Conditionally, inside an if or a loop. use() is unusual: it's allowed inside conditionals and loops, unlike the rules-of-hooks for other hooks.

What does useActionState return?

  • [value, setValue]
  • [state, formAction, isPending]
  • [data, error, loading]
  • [dispatch, state]

Answer: [state, formAction, isPending]. useActionState(action, initialState) returns [state, formAction, isPending].

A useActionState action function receives…

  • The previous state and the form data, and returns the next state
  • Only the form data
  • Just the event object
  • The component's props

Answer: The previous state and the form data, and returns the next state. The action gets (prevState, formData) and returns the next state — ideal for validation errors or a success message.

Which hook lets a reusable child SubmitButton know the form is submitting?

  • useActionState
  • useOptimistic
  • useFormStatus
  • useState

Answer: useFormStatus. useFormStatus, read by a child inside the <form>, exposes { pending } without prop drilling.

useFormStatus is imported from…

  • react
  • react-dom
  • react-router-dom
  • @reduxjs/toolkit

Answer: react-dom. useFormStatus comes from react-dom and only works inside a <form>.

With useOptimistic, what happens if the real action fails?

  • You must manually undo the change
  • The UI stays on the optimistic value
  • It throws to an error boundary
  • React discards the optimistic value and re-renders from the real state

Answer: React discards the optimistic value and re-renders from the real state. The optimistic value is disposable; React drops it and shows the true state, so the UI snaps back automatically.

useFormStatus must be read…

  • In the same component that renders the <form>
  • In a child component inside the <form>
  • At the app root
  • Inside useEffect

Answer: In a child component inside the <form>. It reads the nearest enclosing form, so it belongs in a child, not the component that renders the form.

Around a component that calls use() on a promise, you should wrap…

  • Nothing extra is needed
  • A Redux Provider
  • A <Suspense> boundary and an error boundary
  • A useMemo

Answer: A <Suspense> boundary and an error boundary. Suspense catches the pending state and an error boundary catches rejection; both are needed.

Continue this course

Related lessons