React 19 New Features: Actions, useActionState, useOptimistic, use and More

React.js Full Course: Beginner to Advanced · Advanced React · 10 min read · Updated

A form action in React 19

  1. You pass a function to the form action. React gives it the FormData.
  2. useActionState keeps the result, an error message and the pending flag for you.
  3. useOptimistic shows the expected result on screen right away.
  4. Meanwhile the real request goes to the server.
  5. If it works, the real data replaces the guess. If it fails, the guess disappears and you show an error.

React 19 added several features that make common jobs, such as forms and data loading, shorter and easier. You do not need them to build good apps, and older code that uses the earlier ways keeps working. But you will see these tools in new projects, so it is good to know what they do. This lesson explains each one with a small example.

The big idea: Actions

An action is a function that does something with data, usually asynchronous, such as sending a form to a server. React 19 lets you pass a function to a form's action prop. React runs it for you when the form is submitted, handles the “pending” period, and resets the form fields after a good submit.

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

  return (
    <form action={subscribe}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <button type="submit">Subscribe</button>
    </form>
  );
}

Compare this with the forms in lessons 9 and 19: there is no onSubmit, no preventDefault, no FormData call, and no state for the fields. React gives your function the FormData for you. The form also works as a plain HTML form if JavaScript is slow to load.

useActionState: results, errors and pending

An action usually needs to give something back, like an error message or a success note. useActionState connects an action to state:

import { useActionState } from 'react';

async function signUpAction(previousState, formData) {
  const email = formData.get('email');
  const password = formData.get('password');

  if (password.length < 8) {
    return { error: 'Your password needs at least 8 characters.' };
  }
  const response = await fetch('/api/signup', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!response.ok) return { error: 'Could not create the account. Please try again.' };
  return { success: true };
}

function SignUpForm() {
  const [state, formAction, isPending] = useActionState(signUpAction, {});

  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" required />

      <button disabled={isPending}>{isPending ? 'Creating...' : 'Create account'}</button>

      {state.error && <p role="alert">{state.error}</p>}
      {state.success && <p role="status">Account created. Welcome!</p>}
    </form>
  );
}
  • The action receives the previous state as its first argument, and the FormData as the second.
  • Whatever the action returns becomes the new state.
  • useActionState gives you [state, formAction, isPending]. Use formAction in the form, and isPending to disable the button.

Forms that need validation, errors and a loading state become much shorter. Remember, as always, to validate again on the server.

useFormStatus: a button that knows

A submit button deep in a component tree can know whether its parent form is sending, without any props, using useFormStatus from react-dom:

import { useFormStatus } from 'react-dom';

function SubmitButton({ children }) {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>{pending ? 'Sending...' : children}</button>;
}

// use it inside any form
<form action={subscribe}>
  <input name="email" type="email" />
  <SubmitButton>Subscribe</SubmitButton>
</form>

It must be used in a component that is inside the form, not in the component that renders the form itself.

useOptimistic: update at once, fix later

When someone clicks a heart or sends a chat message, waiting for the server makes the page feel slow. An optimistic update shows the expected result immediately, and returns to the real state if the request fails. useOptimistic makes this easy:

import { useOptimistic, useState } from 'react';

function Messages({ initialMessages, sendToServer }) {
  const [messages, setMessages] = useState(initialMessages);
  const [optimistic, addOptimistic] = useOptimistic(
    messages,
    (current, newText) => [...current, { id: 'temp-' + newText, text: newText, sending: true }]
  );

  async function send(formData) {
    const text = formData.get('text');
    addOptimistic(text);                       // appears at once, marked "sending"
    const saved = await sendToServer(text);    // real request
    setMessages((current) => [...current, saved]);
  }

  return (
    <div>
      <ul>
        {optimistic.map((m) => (
          <li key={m.id} style={{ opacity: m.sending ? 0.5 : 1 }}>{m.text}</li>
        ))}
      </ul>
      <form action={send}>
        <input name="text" required />
        <button>Send</button>
      </form>
    </div>
  );
}

The optimistic value is used only while the action runs. When it finishes, React uses the real messages again. If the request failed, the temporary message simply disappears, since the real state never included it. Show an error message to explain.

The use API

use is a new function that reads a value that may not be ready yet. It can read a promise, and it can read context. When given a promise, it pauses the component until the promise is done, and shows the nearest Suspense fallback meanwhile.

import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise);   // waits here
  return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}

function Post({ commentsPromise }) {
  return (
    <Suspense fallback={<p>Loading comments...</p>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

An important point: the promise should be created outside the rendering component, for example by a parent, a router loader, a framework or a data library, and passed down. If you create a new promise inside the component that calls use, it will create a new one every render and loop. For most apps, a library such as TanStack Query or a framework handles this for you. Unlike other hooks, use can be called inside an if or a loop. When reading context, use(MyContext) works like useContext.

ref as a normal prop

Before React 19, passing a ref through your own component needed forwardRef. Now ref is simply a prop that function components can receive:

function TextInput({ ref, label, ...rest }) {
  return (
    <label>
      {label}
      <input ref={ref} {...rest} />
    </label>
  );
}

Refs can also have a cleanup function now: a ref callback may return a function that React calls when the element is removed.

Document metadata in components

You can now write title, meta and link tags anywhere in your components, and React moves them into the head of the page for you:

function ProductPage({ product }) {
  return (
    <article>
      <title>{product.name} | My Shop</title>
      <meta name="description" content={product.summary} />
      <h1>{product.name}</h1>
    </article>
  );
}

This replaces the need for a helper library for simple cases. Remember that a page drawn only in the browser is harder for search engines to read, so for pages that must rank well, think about server rendering.

Smaller improvements

  • Context as a provider: you can write <ThemeContext value="dark"> instead of <ThemeContext.Provider value="dark">.
  • Better error reporting: clearer messages, and options to report errors caught and not caught.
  • Support for custom elements (web components).
  • Stylesheets and scripts: better control of loading order and avoiding duplicates.
  • Preloading helpers such as preload and preconnect in react-dom.

The React Compiler and Server Components

Two bigger ideas go with this release. The React Compiler is a build tool that adds memoisation for you, so you write less useMemo and useCallback. Server Components let some components run only on the server and send results to the browser, which is used by frameworks such as Next.js. You do not need either to start. Learn them after you are comfortable with everything in this course.

Should you use these features now?

  • If you start a new project on React 19, yes: form actions and useActionState make forms shorter.
  • If your project uses an older version, the old ways (onSubmit, useState) still work, and you can upgrade later.
  • Keep using a data library for server data. use is a building block, not a replacement for caching.

Common mistakes

  • Calling useFormStatus in the same component that renders the form. It must be in a child.
  • Creating the promise inside the component that calls use.
  • Forgetting name on inputs, so the FormData has no values.
  • Trusting only the client check. Always validate on the server.
  • Using optimistic updates for actions that often fail.

Your task

  1. Make a newsletter form with a form action and no onSubmit.
  2. Rewrite your sign-up form with useActionState and show the pending state and error.
  3. Add a SubmitButton that uses useFormStatus.
  4. Add an optimistic message list that shows new messages at once.

How to check it worked

  • The form works with no preventDefault code.
  • The button is disabled and shows “Creating...” while the request runs.
  • A new message appears immediately, and disappears if you make the fake request fail.

Summary

  • Form action takes a function that receives the FormData. useActionState adds result, error and pending state.
  • useFormStatus lets a child button know that its form is sending. useOptimistic shows expected results at once.
  • use reads a promise or context, and works with Suspense. ref is now a normal prop.
  • You can render title and meta tags in components. The old ways still work.

Frequently asked questions

What is new in React 19?

Form actions, useActionState, useFormStatus, useOptimistic, the use API, ref as a prop, document metadata in components, and other smaller improvements.

Do I have to upgrade to React 19?

No. Older ways keep working. Upgrade when you are ready, or start new projects on the latest version.

What is useOptimistic?

A hook that shows an expected result immediately while a request is running, and goes back to the real state if the request fails.

Is use the same as useEffect for fetching?

No. use reads a promise that was created outside the component and waits with Suspense. Data libraries or frameworks usually create and cache those promises.