hook useActionState new in React 19

Date Published

useActionState_hook_react_19.png

Summary

With the useActionState hook, you can manage your

  1. Your Form
  2. Your serverAction that runs once submitting the form.
  3. The errors and that may come as a result of executing your serverAction e.g : validation errors form or db errors
  4. The transition period between the time of submit and the time of results (errors, succesmessages) coming back.

This is how it looks, i omitted the class-values.
The repo you can find here : github : useActionState-Hook-Demo

The form

1'use client';
2
3import { useActionState } from 'react';
4import { createTask } from '../actions/actions';
5
6export default function Form() {
7 // useActionState is a hook that allows us to handle actions in a component
8 // input parameters useActionState :
9 // createTask is the action function that will handle the input of the form
10 // null is the initial state
11 // output hook :
12 // an action function to be used by submitting the form
13 // the state : eventual errors generated by createTask
14 // isPending is a boolean that indicates if the action is pending
15 const [error, action, isPending] = useActionState(createTask, null);
16
17 return (
18 //action is created by useActionState and enhances it with the ability to handle errors and loading states
19 <form action={action} className="flex flex-col gap-4">
20 <div>
21 <input
22 type="text"
23 name="content"
24 placeholder="Add a new task..."
25 disabled={isPending}
26 />
27 </div>
28 <button
29 disabled={isPending}
30 >
31 {isPending ? (
32 <span className="flex items-center justify-center gap-2">
33 <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
34
35 </svg>
36 Adding...
37 </span>
38 ) : (
39 'Add Task'
40 )}
41 </button>
42 {error && (
43 <p className="text-red-500 dark:text-red-400 text-sm text-center bg-red-50 dark:bg-red-900/20 py-2 px-3 rounded-lg">
44 {error}
45 </p>
46 )}
47 </form>
48 );
49}

Server action code

This is the serverAction : createTask.tsx given as a parameter to the useActionState hook in the form
the hook creates an action that is enhanced with server and transition logic.

1// Mark this file as a server action module - all exports run on the server
2'use server';
3
4import prisma from '@/lib/db';
5import { revalidatePath } from 'next/cache';
6
7// Server action for creating a new task
8// previousState: previous error state from useActionState hook
9// formData: form data submitted from the client
10export async function createTask(previousState: any, formData: FormData) {
11 // Simulate network delay
12 await new Promise((resolve) => setTimeout(resolve, 1000));
13 try {
14 // Extract content from form data
15 const content = formData.get('content');
16
17 // Validate content field
18 if (!content || typeof content !== 'string') {
19 return 'Content is required';
20 }
21
22 // Create task in database
23 await prisma.task.create({ data: { content } });
24 } catch (e) {
25 return 'an error occurred';
26 }
27 // Revalidate the home page cache to show new task
28 revalidatePath('/');
29}
30
31// Server action for deleting a task
32// previousState: previous error state from useActionState hook
33// id: task ID to delete
34export async function deleteTask(previousState: any, id: number) {
35 // Simulate network delay
36 await new Promise((resolve) => setTimeout(resolve, 1000));
37 try {
38 // Delete task from database
39 await prisma.task.delete({ where: { id } });
40 } catch (e) {
41 return 'an error occurred';
42 }
43 // Revalidate the home page cache to remove deleted task
44 revalidatePath('/');
45}

There are two advantages using this hook

  1. Errors and transitions are taken care of by the useStateAction hook
  2. It can be used for submitting forms as well as launching pure actions.

How to use this pattern for a pure serveraction

Jsx for displaying the list of tasks

className values are omitted check full code here : github : useActionState-Hook-Demo

1import DeleteBtn from './components/delete-btn';
2import Form from './components/form';
3import prisma from '@/lib/db';
4
5// Server Component - runs on the server and can directly access the database
6export default async function Home() {
7 // Fetch all tasks from database - this runs on the server
8 const tasks = await prisma.task.findMany();
9
10 return (
11 <main>
12 <div className="w-full max-w-md">
13 {/* Header section */}
14 <div>
15 <h1>
16 My Tasks
17 </h1>
18 <p>
19 Stay organized and productive
20 </p>
21 </div>
22
23 {/* Form component for adding new tasks */}
24 <div>
25 <Form />
26 </div>
27
28 {/* Render tasks list if tasks exist, otherwise show empty state */}
29 {tasks.length > 0 ? (
30 <ul className="space-y-3">
31 {tasks.map((task) => (
32 <li
33 key={task.id}
34 >
35 <span>
36 {task.content}
37 </span>
38 {/* Delete button - only visible on hover */}
39 <DeleteBtn id={task.id} />
40 </li>
41 ))}
42 </ul>
43 ) : (
44 <div>
45 <p>
46 No tasks yet. Add one to get started! 🚀
47 </p>
48 </div>
49 )}
50 </div>
51 </main>
52 );
53}

Jsx for the delete-button

When not using a Form we have to use the startTransition function to wrap the server action.
This is needed to keep the UI responsive during the time the action is executed

1'use client';
2import { useActionState, startTransition } from 'react';
3import { deleteTask } from '../actions/actions';
4
5interface DeleteBtnProps {
6 id: number;
7}
8
9export default function DeleteBtn({ id }: DeleteBtnProps) {
10 // useActionState manages the server action state
11 // First element (error) is ignored, action is the wrapped function, isPending tracks loading state
12 const [, action, isPending] = useActionState(deleteTask, null);
13
14 // startTransition marks this as a non-urgent update, keeping UI responsive
15 // React can interrupt it for urgent updates (user input) and shows isPending state
16 // Required when calling server actions from event handlers (not needed for forms)
17 const handleClick = () => {
18 startTransition(() => action(id));
19 };
20
21 return (
22 <button
23 onClick={handleClick}
24 disabled={isPending}
25 aria-label="Delete task"
26 >
27 {isPending ? (
28 <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
29 <circle
30 className="opacity-25"
31 cx="12"
32 cy="12"
33 r="10"
34 stroke="currentColor"
35 strokeWidth="4"
36 fill="none"
37 />
38 <path
39 className="opacity-75"
40 fill="currentColor"
41 d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
42 />
43 </svg>
44 ) : (
45 <svg
46 className="w-4 h-4"
47 fill="none"
48 stroke="currentColor"
49 viewBox="0 0 24 24"
50 >
51 <path
52 strokeLinecap="round"
53 strokeLinejoin="round"
54 strokeWidth={2}
55 d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
56 />
57 </svg>
58 )}
59 </button>
60 );
61}


Many thanks to bytegrad for this youtube video on which this post is inspired !