Skip to content

Exploring React 19 Hooks

The React 19 hooks useFormStatus, useActionState, useOptimistic and use, explained with current code examples and the mistakes to avoid.
GuideUpdated 5 min readDevelopmentReact Js

React 19 has been stable since 5 December 2024, and it added a small set of hooks built around Actions: functions that run during a form submission or a transition, with React tracking whether they are still pending. This article covers the four you are most likely to use, useFormStatus, useActionState, useOptimistic and use, with examples that match the official React documentation.

1. useFormStatus

useFormStatus tells a component whether the form it sits inside is being submitted. It lives in react-dom, not react, because it is about DOM forms.

import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

export default function ContactForm({ sendMessage }) {
  return (
    <form action={sendMessage}>
      <input name="email" type="email" required />
      <SubmitButton />
    </form>
  );
}

What it returns

useFormStatus takes no arguments and returns an object with four fields:

  • pending: true while the parent form is submitting, otherwise false.
  • data: a FormData object with the data being submitted, or null when nothing is being submitted.
  • method: the HTTP method of the form, either 'get' or 'post'.
  • action: a reference to the function passed to the form's action prop, or null if there is no parent form or the action is a URL.

There are no error or success fields. If you need to show the result of a submission, use useActionState below.

Key points

  • It must be called from a component rendered inside a form. In the example, SubmitButton is a child of the form.
  • It only reports on a parent form. Calling it in the same component that renders the form will always give pending as false.
  • The usual use is disabling the submit button or showing a loading state while the request runs.

2. useActionState

useActionState holds state that is updated by an Action, typically a form submission, and tells you whether that Action is still running. You pass it a function and an initial state, and it returns three values.

import { useActionState } from "react";

async function subscribe(previousState, formData) {
  const email = formData.get("email");
  const res = await fetch("/api/subscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email }),
  });

  if (!res.ok) {
    return { error: "Could not subscribe. Please try again.", done: false };
  }
  return { error: null, done: true };
}

export default function Newsletter() {
  const [state, formAction, isPending] = useActionState(subscribe, {
    error: null,
    done: false,
  });

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? "Subscribing..." : "Subscribe"}
      </button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.done && <p>Thanks, you are subscribed.</p>}
    </form>
  );
}

How it works

  • The call is useActionState(action, initialState), with an optional third argument, permalink, used with Server Components for progressive enhancement.
  • It returns [state, dispatchAction, isPending]: the current state, a function to trigger the Action, and a flag that is true while it runs.
  • Your action function receives the previous state as its first argument and the payload as its second. When used as a form action, the payload is the form's FormData. This is the most common mistake: formData is the second argument, not the first.
  • Whatever the action returns becomes the new state, which is how you report errors and success.

Key points

  • Passing dispatchAction to a form's action prop wraps the submission in a transition for you. If you call it yourself, do it inside startTransition, or React logs an error in development.
  • Multiple calls are queued and run in order, and each one receives the result of the previous call.
  • It is a hook, so call it at the top level of a component, not inside conditions or loops.

3. useOptimistic

useOptimistic shows a temporary value while an Action is running, so the interface responds immediately instead of waiting for the server. When the Action finishes, React goes back to rendering the real value.

import { useOptimistic, startTransition } from "react";

function LikeButton({ likes, saveLike }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (currentLikes, amount) => currentLikes + amount
  );

  function handleClick() {
    startTransition(async () => {
      addOptimisticLike(1);
      // saveLike sends the request and updates `likes` in the parent.
      await saveLike();
    });
  }

  return <button onClick={handleClick}>Like ({optimisticLikes})</button>;
}

How it works

  • The call is useOptimistic(value, reducer?). value is the real state, here the likes count passed from the parent.
  • The optional reducer receives the current state and whatever you pass to the setter, and must return the next optimistic state. It must be pure.
  • It returns [optimisticState, setOptimistic]. While an Action is pending, optimisticState shows the reducer's result. Otherwise it is simply value.

Key points

  • Call the setter inside an Action: a startTransition callback, as above, or a form action. Called outside one, React shows a warning and the optimistic value only renders briefly.
  • You do not write rollback code. If the request fails and the parent never updates value, the button goes back to the old count when the Action ends.
  • Use it for actions that usually succeed, such as likes, sending a chat message or adding to a basket.

4. use

use reads the value of a Promise or a context during render. Strictly speaking, React calls it an API rather than a hook, because unlike hooks it can be called inside conditions and loops.

Its most common use is streaming data from a Server Component to a Client Component: the server starts the request without awaiting it, and the client component reads the result with use, inside a Suspense boundary.

// page.js (Server Component)
import { Suspense } from "react";
import { Message } from "./message";
import { fetchMessage } from "./lib";

export default function Page() {
  const messagePromise = fetchMessage(); // started here, not awaited
  return (
    <Suspense fallback={<p>Loading message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

// message.js (Client Component)
"use client";

import { use } from "react";

export function Message({ messagePromise }) {
  const message = use(messagePromise);
  return <p>{message}</p>;
}

Key points

  • While the Promise is pending, the nearest Suspense boundary shows its fallback. If it rejects, the nearest Error Boundary shows its fallback.
  • Do not create the Promise during render, for example use(fetch(url)) inside the component. A new Promise on every render makes React show the fallback again and again. Pass in a Promise that was created once, or cached.
  • Do not wrap use in try/catch; use an Error Boundary instead.
  • use can also read context, like useContext, but inside an if statement. Reading context with use is not supported in Server Components.

Other React 19 changes worth knowing

Alongside these hooks, React 19 lets function components receive ref as an ordinary prop, so new components no longer need forwardRef. Form elements also accept functions in their action prop, which is what makes useFormStatus and useActionState work without any extra wiring.

Conclusion: which React 19 hooks to use, and when

Together, these hooks remove most of the hand-written loading and error state that forms used to need. useFormStatus handles the pending button, useActionState holds the result of a submission, useOptimistic makes the interface feel instant, and use reads async data with Suspense. Check the official React documentation for the full API of each before relying on edge cases.

If you want a React application or a set of fast, reliable forms built for your business, that is part of my custom web development work.

Share this
Build work

Need this built properly?

I build the web and mobile products these notes come out of, in React, React Native and Node. A first conversation costs nothing and often ends with a smaller scope than you expected.

Not ready to book a call? Get a free website health check