hook useActionState new in React 19
Date Published

Summary
With the useActionState hook, you can manage your
- Your Form
- Your serverAction that runs once submitting the form.
- The errors and that may come as a result of executing your serverAction e.g : validation errors form or db errors
- 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';23import { useActionState } from 'react';4import { createTask } from '../actions/actions';56export default function Form() {7 // useActionState is a hook that allows us to handle actions in a component8 // input parameters useActionState :9 // createTask is the action function that will handle the input of the form10 // null is the initial state11 // output hook :12 // an action function to be used by submitting the form13 // the state : eventual errors generated by createTask14 // isPending is a boolean that indicates if the action is pending15 const [error, action, isPending] = useActionState(createTask, null);1617 return (18 //action is created by useActionState and enhances it with the ability to handle errors and loading states19 <form action={action} className="flex flex-col gap-4">20 <div>21 <input22 type="text"23 name="content"24 placeholder="Add a new task..."25 disabled={isPending}26 />27 </div>28 <button29 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">3435 </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 server2'use server';34import prisma from '@/lib/db';5import { revalidatePath } from 'next/cache';67// Server action for creating a new task8// previousState: previous error state from useActionState hook9// formData: form data submitted from the client10export async function createTask(previousState: any, formData: FormData) {11 // Simulate network delay12 await new Promise((resolve) => setTimeout(resolve, 1000));13 try {14 // Extract content from form data15 const content = formData.get('content');1617 // Validate content field18 if (!content || typeof content !== 'string') {19 return 'Content is required';20 }2122 // Create task in database23 await prisma.task.create({ data: { content } });24 } catch (e) {25 return 'an error occurred';26 }27 // Revalidate the home page cache to show new task28 revalidatePath('/');29}3031// Server action for deleting a task32// previousState: previous error state from useActionState hook33// id: task ID to delete34export async function deleteTask(previousState: any, id: number) {35 // Simulate network delay36 await new Promise((resolve) => setTimeout(resolve, 1000));37 try {38 // Delete task from database39 await prisma.task.delete({ where: { id } });40 } catch (e) {41 return 'an error occurred';42 }43 // Revalidate the home page cache to remove deleted task44 revalidatePath('/');45}
There are two advantages using this hook
- Errors and transitions are taken care of by the useStateAction hook
- 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';45// Server Component - runs on the server and can directly access the database6export default async function Home() {7 // Fetch all tasks from database - this runs on the server8 const tasks = await prisma.task.findMany();910 return (11 <main>12 <div className="w-full max-w-md">13 {/* Header section */}14 <div>15 <h1>16 My Tasks17 </h1>18 <p>19 Stay organized and productive20 </p>21 </div>2223 {/* Form component for adding new tasks */}24 <div>25 <Form />26 </div>2728 {/* 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 <li33 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';45interface DeleteBtnProps {6 id: number;7}89export default function DeleteBtn({ id }: DeleteBtnProps) {10 // useActionState manages the server action state11 // First element (error) is ignored, action is the wrapped function, isPending tracks loading state12 const [, action, isPending] = useActionState(deleteTask, null);1314 // startTransition marks this as a non-urgent update, keeping UI responsive15 // React can interrupt it for urgent updates (user input) and shows isPending state16 // Required when calling server actions from event handlers (not needed for forms)17 const handleClick = () => {18 startTransition(() => action(id));19 };2021 return (22 <button23 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 <circle30 className="opacity-25"31 cx="12"32 cy="12"33 r="10"34 stroke="currentColor"35 strokeWidth="4"36 fill="none"37 />38 <path39 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 <svg46 className="w-4 h-4"47 fill="none"48 stroke="currentColor"49 viewBox="0 0 24 24"50 >51 <path52 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}