Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Advanced 14 min read

use, useOptimistic, useActionState & useFormStatus

The newer hooks around forms, server actions and promises

In one sentence

React 19 brings four building blocks that make forms, loading states and “instantly visible” actions considerably shorter – especially in Next.js.

Good to know This one is optional, not compulsory

You only need these four once you build forms talking to a server. If anything here moves too fast: no harm done – come back later. For everyday work useState, useEffect and the others from the previous lessons are plenty.

use – waiting for a result without useEffect

So far, loading data meant useEffect plus a state for loading plus one for errors. use turns that around: you hand it a promise and get the value straight back. While it is missing, a SuspenseAn area that shows a placeholder (“loading …”) while something inside it is not ready yet. boundary above shows the loading state.

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

function Profile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);      // pauses until the promise resolves
  return <h1>{user.name}</h1>;
}

// The parent takes care of the loading state and errors:
<Suspense fallback={<Skeleton />}>
  <Profile userPromise={loadUser(id)} />
</Suspense>
Good to know Who shows “loading …”?

Not the component itself. It simply pauses, and the nearest <Suspense> boundary above steps in with its fallback. For errors there is the counterpart: an error boundary.

In more depth: use is allowed inside an if optional

use is the only hook the “top level only” rule does not apply to. It may sit inside conditions and loops. That also holds when you read a ContextA kind of pneumatic tube: a value goes in at the top and can be taken out anywhere below. with it:

use-context.tsx
function Row({ highlight }) {
  if (highlight) {
    const theme = use(ThemeContext);   // with useContext this would be forbidden
    return <div style={{ color: theme.accent }}>…</div>;
  }
  return <div>…</div>;
}
Pitfall The promise must not be created while drawing

use(loadUser(id)) written directly in the component would be an endless loop: every pass creates a new promise to wait on. The promise comes from a Server componentA component that only runs on the server. It cannot use hooks and cannot handle clicks.In Next.js components are server components by default. Adding "use client" at the top of the file makes it a client component. or from a cache.

useActionState – form, result and loading state in one

action-state.tsx
"use client";

const [state, formAction, isPending] = useActionState(
  async (previous: State, formData: FormData) => {
    const mail = formData.get("mail") as string;
    if (!mail.includes("@")) return { error: "Please enter a valid address" };
    await signUp(mail);
    return { success: true };
  },
  { error: null }        // starting value
);

return (
  <form action={formAction}>
    <input name="mail" />
    <button disabled={isPending}>{isPending ? "Sending …" : "Sign up"}</button>
    {state.error && <p role="alert">{state.error}</p>}
  </form>
);
No custom onSubmit, no isLoading state, no try/catch scaffolding – and it even works without JavaScript when the action lives on the server.
Tip useFormStatus – for child components

const { pending } = useFormStatus() reads the state of the surrounding form. That lets a generic <SubmitButton> disable itself without any props being passed down. Important: the hook has to live in a component inside the <form>, not in the same one.

useOptimistic – show it now, correct it later

optimistic.tsx
const [messages, setMessages] = useState(initial);

const [visible, addOptimistic] = useOptimistic(
  messages,
  (current: Message[], newText: string) => [
    ...current,
    { text: newText, sending: true },
  ]
);

async function send(formData: FormData) {
  const text = formData.get("text") as string;
  addOptimistic(text);                    // visible immediately
  const saved = await sendToServer(text);
  setMessages((m) => [...m, saved]);
}
If the request fails, React drops the optimistic value automatically – the list snaps back to the real state.
Reach for it when …
  • +

    use for data a server component hands in as a promise

  • +

    useActionState for forms with validation and a server response

  • +

    useFormStatus in reusable submit buttons

  • +

    useOptimistic for likes, comments, toggles – actions that nearly always succeed

Skip it when …
  • useOptimistic for payments or anything that often fails

  • use with a promise you recreate while drawing – endless loop

  • useFormStatus in the same component that renders the <form>

Good to know Next.js App Router

These are client hooks ("use client"), but they are built to work with server actions. The action itself may be a "use server" function – then validation runs on the server and the form works even with JavaScript disabled.

Does it stick?

4 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Optimistic adding

After you submit it takes half a second for the entry to appear – which feels sluggish. Show it immediately (greyed out, with a “…”) and replace it once the answer arrives.

  • Right after the click the new text is in the list
  • Once the simulated request finishes it is in there for good
  • Afterwards the entry appears exactly once
import { useOptimistic, useState, useTransition } from "react";

const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));

export default function App() {
  const [items, setItems] = useState<string[]>(["First"]);
  const [text, setText] = useState("");
  const [, start] = useTransition();

  // TODO: derive the optimistic list

  function send() {
    if (!text.trim()) return;
    const value = text;
    setText("");
    start(async () => {
      // TODO: show it optimistically first
      await wait(500);
      setItems((p) => [...p, value]);
    });
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <input aria-label="text" value={text} onChange={(e) => setText(e.target.value)} />{" "}
      <button onClick={send}>Send</button>
      <ul>
        {items.map((i, idx) => (
          <li key={idx}>{i}</li>
        ))}
      </ul>
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →