ยท

Simplify Form Handling in React 19

React 19's new hook simplifies handling asynchronous operations in React, especially when dealing with forms

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:

"use client";
 
export const FormAction = () => {
  return (
    <div>
      <form>
        <input type="text" name="name" value="Hello from actions" readOnly />
        <button type="submit">Submit</button>
      </form>
      <footer>
        <p>Awaiting action ๐Ÿš€</p>
      </footer>
    </div>
  );
};

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.

// canary.d.ts
export function useActionState<State, Payload>(
  action: (state: Awaited<State>, payload: Payload) => State | Promise<State>,
  initialState: Awaited<State>
): [
  state: Awaited<State>,
  dispatch: (payload: Payload) => void,
  isPending: boolean
];

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

"use client";
 
import { useActionState } from "react";
 
type State = { data: string; status: "SUCCESS" } | { status: "ERROR" | "INIT" };
const initState: State = { status: "INIT" };
 
export const FormAction = () => {
  const [action, submitAction, isPending] = useActionState(
    async (prevState: State, formData: FormData) =>
      runAction(prevState, String(formData.get("name"))),
    initState
  );
 
  return (
    <div>
      <form action={submitAction}>
        <input type="text" name="name" value="Hello from actions" readOnly />
        <button type="submit" disabled={isPending}>
          Submit
        </button>
      </form>
      <footer>
        {action.status === "INIT" && <p>Awaiting action ๐Ÿš€</p>}
        {action.status === "SUCCESS" && <p>Success, all good โœ…</p>}
        {action.status === "ERROR" && <p>Error, please resubmit action โŒ</p>}
        <code>{JSON.stringify({ isPending })}</code>
        <code>{JSON.stringify(action)}</code>
      </footer>
    </div>
  );
};
  • 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.

async function runAction(_prevState: State, data: string) {
  return new Promise<State>((r) => {
    setTimeout(
      () =>
        Math.random() < 0.5
          ? r({ data, status: "SUCCESS" })
          : r({ status: "ERROR" }),
      1500
    );
  });
}

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