My motive for learning the useActionState hook is to make sense of the Conform library. I could just use the library, but I can't explain its behavior. Therefore, it's important for me to understand it deeply.
Signature
The useActionState hook receives an action and an initial state, and returns a state, a dispatch function, and a flag of pending transition. The action process a payload along with the previous state and returns a result of the same type.
export function useActionState<State, Payload>(
action: (state: Awaited<State>, payload: Payload) => State | Promise<State>,
initialState: Awaited<State>,
permalink?: string,
): [
state: Awaited<State>,
dispatch: (payload: Payload) => void,
isPending: boolean,
];Using with a form
In the scenario of using the hook with the Conform library, the payload is FormData. Looking at the return values, the dispatch function can be used as a form action.
For example, here is a server action for form submission:
"use server";
import { parseWithZod } from "@conform-to/zod/v4";
import { authorAttendanceSchema } from "@/features/author/schemas/author-attendance-schema";
import { SubmissionResult } from "@conform-to/react";
export const createAuthorAttendanceAction = async (
_previousState: SubmissionResult<string[]> | undefined,
formData: FormData,
): Promise<SubmissionResult<string[]> | undefined> => {
const submission = parseWithZod(formData, {
schema: authorAttendanceSchema,
});
if (submission.status !== "success") {
return submission.reply();
}
// Log key-value pairs of formData
for (const p of formData) {
console.log(p);
}
// Create a POST request
return submission.reply({ resetForm: true });
};And here is how to use the useActionState hook:
"use client";
import type { AuthorListItem } from "@/features/author/types";
import { useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod/v4";
import { useActionState } from "react";
import { authorAttendanceSchema } from "@/features/author/schemas/author-attendance-schema";
import { authorAttendanceAction } from "@/features/author/actions/author-attendance-action";
type Props = {
authors: AuthorListItem[];
};
export const AuthorAttendanceFormPresenter = ({ authors }: Props) => {
const [lastResult, action] = useActionState(authorAttendanceAction, undefined);
const [form, fields] = useForm({
lastResult,
onValidate: ({ formData }) => {
return parseWithZod(formData, { schema: authorAttendanceSchema });
},
});
return (
<form id={form.id} onSubmit={form.onSubmit} action={action} noValidate>
<div>
<label>Attendances</label>
<div>
{authors.map((author) => (
<div key={author.id}>
<label>
<input type="checkbox" name={fields.presentAuthorIds.name} value={author.id} />
{author.name}
</label>
</div>
))}
</div>
</div>
<button type="submit">Submit</button>
</form>
);
}When the form is submitted, this is what appears in the console.
(We can see the payload [("presentAuthorIds", "1")])
["$ACTION_REF_1", ""][
("$ACTION_1:0",
'{"id":"7f7572b552a73cec104ff8c8b821c2bd261141aaef","bound":"$@1"}')
][("$ACTION_1:1", "[null]")][
("$ACTION_KEY", "k6197c500c1eb7bc6016dbd5360e57f2b")
][("presentAuthorIds", "1")];