Webscope.io Logo
Development
8 min read
May 6, 2024

Simplify Form Handling in React 19: Introducing `useActionState` Hook

This is a blog image

React 19's new useActionState hook simplifies handling asynchronous operations in React, especially when dealing with forms. Let's delve into a practical example to see it in action.

Here we have a simple read-only form ready for submission:

1"use client";
2
3export const FormAction = () => {
4  return (
5    <div>
6      <form>
7        <input type="text" name="name" value="Hello from actions" readOnly />
8        <button type="submit">Submit</button>
9      </form>
10      <footer>
11        <p>Awaiting action 🚀</p>
12      </footer>
13    </div>
14  );
15};

The useActionState hook takes a minimum of two arguments: (1) an asynchronous action, that in turn takes its own arguments of previousState and a generic payload, and (2) an initial state. This hook returns (a) the awaited state, (b) a dispatcher and (c) an isPending boolean.

1// canary.d.ts
2export function useActionState<State, Payload>(
3  action: (state: Awaited<State>, payload: Payload) => State | Promise<State>,
4  initialState: Awaited<State>
5): [
6  state: Awaited<State>,
7  dispatch: (payload: Payload) => void,
8  isPending: boolean
9];

So let's define our state's type definition along with our initial state value to begin shaping our component.

1"use client";
2
3import { useActionState } from "react";
4
5type State = { data: string; status: "SUCCESS" } | { status: "ERROR" | "INIT" };
6const initState: State = { status: "INIT" };
7
8export const FormAction = () => {
9  const [action, submitAction, isPending] = useActionState(
10    async (prevState: State, formData: FormData) =>
11      runAction(prevState, String(formData.get("name"))),
12    initState
13  );
14
15  return (
16    <div>
17      <form action={submitAction}>
18        <input type="text" name="name" value="Hello from actions" readOnly />
19        <button type="submit" disabled={isPending}>
20          Submit
21        </button>
22      </form>
23      <footer>
24        {action.status === "INIT" && <p>Awaiting action 🚀</p>}
25        {action.status === "SUCCESS" && <p>Success, all good ✅</p>}
26        {action.status === "ERROR" && <p>Error, please resubmit action ❌</p>}
27        <code>{JSON.stringify({ isPending })}</code>
28        <code>{JSON.stringify(action)}</code>
29      </footer>
30    </div>
31  );
32};
  • Notice how submitAction, a function generated by useActionState, is used directly in the form's action attribute. This moves away from the form pattern of using callbacks onSubmit.
  • The submission button is disabled based on isPending which allows us to manage state effectively.
  • As for the form's feedback mechanism, it responds dynamically to changes in action's state.

The runAction function here is a mock, simulating an API call which randomly succeds or fails returning a new state, updating the form's status to either SUCCESS or ERROR. This could or could not be a Server Action.

1async function runAction(_prevState: State, data: string) {
2  return new Promise<State>((r) => {
3    setTimeout(
4      () =>
5        Math.random() < 0.5
6          ? r({ data, status: "SUCCESS" })
7          : r({ status: "ERROR" }),
8      1500
9    );
10  });
11}

Gotcha: while this pattern allows you to keep your UI responsive, you need to design how to handle errors. This hook doesn't return an error member, so regardless if you are using a Server Action (where you can't throw errors) or not, you might have to follow the pattern showcased here integrating errors in the action's state.

Why not leverage useActionState in your next React project? What do you think? Does it make it easier or not to to manage state, side effects and boilerplate in form operations?

Here are some resources that we'd recommend you explore:

Check out the code snippets and demo in webscopeio/examples/tree/main/use-action-state

Hector SosaSoftware Developer