Understanding the signature of useActionState hook in React 19

React

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")];