React 19 Actions: forms, useActionState and useOptimistic

By

Published Updated 2 min read

React 19 made async mutations a first-class concept. A function passed to a form's action prop is an Action: React tracks its pending state, handles errors, and resets the form on success.

A form with useActionState

TypeScript / React
'use client';


import { useActionState } from 'react';


type State = { error?: string; ok?: boolean };


async function subscribe(prev: State, formData: FormData): Promise<State> {
  const email = String(formData.get('email') ?? '');
  const res = await fetch('/api/subscribe', {
    method: 'POST',
    body: JSON.stringify({ email }),
  });
  return res.ok ? { ok: true } : { error: 'Could not subscribe' };
}


export function SubscribeForm() {
  const [state, action, pending] = useActionState(subscribe, {});


  return (
    <form action={action}>
      <input name="email" type="email" required />
      <button disabled={pending}>{pending ? 'Sending…' : 'Subscribe'}</button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.ok && <p>Thanks!</p>}
    </form>
  );
}

No useState for loading and error, no manual preventDefault.

useFormStatus for nested components

A submit button deep in the tree can read the parent form's status:

TypeScript / React
import { useFormStatus } from 'react-dom';


function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
}

Optimistic updates with useOptimistic

Show the result immediately and let React roll back if the request fails.

TypeScript / React
'use client';


import { useOptimistic } from 'react';


export function Comments({ comments, send }: Props) {
  const [optimistic, addOptimistic] = useOptimistic(
    comments,
    (current, text: string) => [...current, { id: 'tmp', text, sending: true }],
  );


  async function action(formData: FormData) {
    const text = String(formData.get('text'));
    addOptimistic(text);
    await send(text);
  }


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

Server Actions in Next.js

In Next.js you can pass a Server Action to action directly and the form even works before JavaScript loads:

TypeScript / React
async function createPost(formData: FormData) {
  'use server';
  await db.post.create({ data: { title: String(formData.get('title')) } });
}


export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button>Create</button>
    </form>
  );
}

Always validate input on the server. A Server Action is a public endpoint.

Sources and further reading