useActionData

useActionData() returns whatever the current route's action function returned, from its most recent call. Unlike useLoaderData(), there's nothing to show at first: useActionData() is undefined until a submission has actually been made to that route's action during this session, which makes it a natural fit for redisplaying validation errors right next to the form that produced them.

Submitting a <Form method="post"> (imported from "react-router", not a plain HTML form) posts its data to the current route's action instead of causing a full page reload. The action reads the submission with request.formData(), and can either return data directly – picked up by useActionData() in the component that rendered the form – or return redirect() to send the user somewhere else, in which case there's no form left around to call useActionData() on that route.


import React from "react";
import { createBrowserRouter, Form, useActionData, redirect } from "react-router";
import { RouterProvider } from "react-router/dom";

const router = createBrowserRouter([
  {
    path: "/signup",
    element: <SignupForm />,
    action: async ({ request }) => {
      let formData = await request.formData();
      let username = formData.get("username") || "";

      if (username.length < 3) {
        return { error: "Username must be at least 3 characters." };
      }

      await createUser(username);
      return redirect("/welcome");
    },
  },
  { path: "/welcome", element: <h2>Welcome aboard!</h2> },
]);

export default function UseActionDataExample() {
  return <RouterProvider router={router} />;
}

function SignupForm() {
  let actionData = useActionData();

  return (
    <Form method="post">
      <p>
        <label>
          Username: <input name="username" />
        </label>
      </p>
      {actionData?.error && (
        <p style={{ color: "red" }}>{actionData.error}</p>
      )}
      <button type="submit">Sign up</button>
    </Form>
  );
}

function createUser(username) {
  return fetch("/api/users", {
    method: "POST",
    body: JSON.stringify({ username }),
  });
}

A redirect() from an action is why useActionData() is only ever read on the same route that owns the action – once the user has been sent elsewhere, there's nothing left to redisplay. Compare with useLoaderData, which always has something to return the moment its route renders.