Frontend Development 18 min read

Using React 19 useActionState and Server Actions in Next.js – A Complete Guide

This article explains the new React 19 useActionState hook, how it replaces useFormState/useFormStatus, and demonstrates its integration with Next.js Server Actions through four progressive examples, covering traditional implementations, the deprecated hooks, the new hook usage in forms and non‑form components, and best‑practice patterns.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Using React 19 useActionState and Server Actions in Next.js – A Complete Guide

Preface

On April 25, 2024, React announced the React 19 RC blog, introducing a new hook – useActionState .

Although it is presented as a new hook, it is essentially the Canary version of useFormState with added functionality and a new name.

Using useActionState can replace the previous useFormState and useFormStatus hooks and works perfectly with Next.js Server Actions.

This article details why useActionState exists and how to combine it with Server Actions in a Next.js project.

Using Next.js 15 with React 19

Create a Next.js project:

npx create-next-app@latest

Both Next.js 15 and React 19 are in RC; upgrade the dependencies:

npm i next@rc react@rc react-dom@rc

If dependency conflicts occur, force the installation:

npm i next@rc react@rc react-dom@rc --force

1. Traditional implementation

Typical React data‑mutation flow: call an API, then update state based on the response (e.g., update a list, show an error).

Project structure:

app
└─ form
   ├─ actions.js
   ├─ form.js
   └─ page.js

app/form/page.js :

import { findToDos } from './actions';
import AddToDoForm from './form';

export default async function Page() {
  const todos = await findToDos();
  return (
{todos.map((todo, i) =>
{todo}
)}
);
}

app/form/form.js (traditional state handling):

'use client'

import { useState } from 'react';
import { createToDo } from './actions';

export default function AddToDoForm() {
  const [todo, setTodo] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    const error = await createToDo(todo);
    setIsPending(false);
    if (error) {
      setError(error);
      return;
    }
  };

  return (
setTodo(e.target.value)} />
{isPending ? 'Adding' : 'Add'}
{error &&
{error}
}
);
}

The drawback is that developers must manually manage pending and error states.

app/form/actions.js (simulated server logic):

'use server'

import { revalidatePath } from "next/cache";

const sleep = ms => new Promise(r => setTimeout(r, ms));
let data = ['阅读', '写作', '冥想'];

export async function findToDos() {
  return data;
}

export async function createToDo(todo) {
  await sleep(500);
  if (Math.random() < 0.5) {
    return '创建失败';
  }
  data.push(todo);
  revalidatePath("/form");
}

The example demonstrates a basic Next.js Server Action with success and failure UI feedback.

2. Deprecated useFormState & useFormStatus

React previously offered useFormState and useFormStatus to automate pending and error handling.

Project structure for the updated example:

app
└─ form2
   ├─ actions.js
   ├─ form.js
   └─ page.js

app/form2/page.js (same as before, just imports the new form component).

app/form2/form.js (using the deprecated hooks):

'use client'

import { useFormState, useFormStatus } from 'react-dom';
import { createToDo } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
{pending ? 'Adding' : 'Add'}
);
}

export default function AddToDoForm() {
  const [state, formAction] = useFormState(createToDo, '');
  return (
{state &&
{state}
}
);
}

The code volume shrinks because developers no longer need to manage pending or error state manually.

Note: useFormStatus must be used inside a separate component (e.g., <SubmitButton> ) to read the parent form’s status.

app/form2/actions.js is identical to the previous actions.js but returns the same error string.

3. React 19 useActionState

In React 19 RC, useFormState is renamed to useActionState . The new hook also returns an isPending flag, eliminating the need for useFormStatus .

Example usage:

const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) => {
    // ...
  },
  null,
);

When the first element is named error it reflects failure messages; a more generic name like state is often clearer.

Project structure for the useActionState example:

app
└─ form3
   ├─ actions.js
   ├─ form.js
   └─ page.js

app/form3/form.js (full useActionState implementation):

'use client'

import { useActionState } from "react";
import { createToDo } from './actions';

export default function AddToDoForm() {
  const [state, formAction, isPending] = useActionState(createToDo, '');
  return (
{isPending ? 'Adding' : 'Add'}
{state &&
{state}
}
);
}

The behavior matches the previous useFormState / useFormStatus example.

4. React 19 useFormStatus

Even though useFormState is renamed, useFormStatus still works and can be useful when a deep component tree needs to read the parent form’s pending state without prop‑drilling.

Example component:

import { useFormStatus } from 'react-dom';

function DesignButton() {
  const { pending } = useFormStatus();
  return
;
}

5. useActionState beyond forms

The hook is not limited to forms. It can be used with any element that triggers an async action, such as a delete button.

Project structure for a non‑form usage:

app
└─ form4
   ├─ actions.js
   ├─ form.js
   ├─ delbtn.js
   └─ page.js

app/form4/delbtn.js (delete button using useActionState):

'use client'

import { useActionState } from "react";
import { deleteTodo } from './actions';

export default function DeleteBtn({ id }) {
  const [state, action, isPending] = useActionState(deleteTodo, null);
  return (
action(id)}
      className="bg-indigo-600 disabled:bg-gray-500 py-2 rounded text-white px-2 ml-2"
    >
      {isPending ? 'Deleting' : 'Delete'} {state}
);
}

The button shows a pending state and renders any error returned by the server action.

Common mistake: awaiting the action function directly. The result is always undefined ; the returned value must be read from the state variable.

Correct pattern without useActionState :

'use client'

import { deleteTodo } from './actions';

export default function DeleteBtn({ id }) {
  return (
{
        const response = await deleteTodo(id);
        if (response) alert(response);
      }}
      className="bg-indigo-600 disabled:bg-gray-500 py-2 rounded text-white px-2 ml-2"
    >
      Delete
);
}

When using useActionState , retrieve the server‑action result from state instead of awaiting the returned promise.

Summary

This article walked through how React/Next.js handles data‑mutation scenarios, starting from a manual implementation, moving to useFormState + useFormStatus , and finally to the new useActionState hook. The four code examples illustrate the reduction in boiler‑plate and automatic management of pending, error, and form reset states.

When working with Next.js Server Actions, pairing them with useActionState is strongly recommended.

frontend developmentReactNext.jsServer ActionsuseActionState
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.