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 byuseActionState
, is used directly in the form'saction
attribute. This moves away from the form pattern of using callbacksonSubmit
. - 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