How React Router Handles Form Actions for Data Mutations

React Router treats <Form> submissions with mutation methods (POST, PUT, PATCH, DELETE) as action requests, routing them through a specialized pipeline that validates CSRF protection, executes the matched route's action function, and handles redirects, errors, or success states before committing results to the router state.

React Router's declarative API transforms standard HTML forms into powerful data mutation primitives. In the remix-run/react-router codebase, the router's core navigation loop distinguishes between data loading and data mutation, triggering a specialized action execution pipeline whenever a form submission uses non-GET methods.

The Mutation Detection Pipeline

Identifying Mutation Methods

When a navigation is triggered via a <Form> submission, the router immediately checks isMutationMethod(submission.formMethod) to determine if the request requires action handling. This validation occurs early in the navigation pipeline at packages/react-router/lib/router/router.ts (lines 1810-1815), ensuring that POST, PUT, PATCH, and DELETE requests are routed to the action execution path while GET requests proceed to standard data loading.

Entering the Submitting State

Upon detecting a mutation, the handleAction function marks the router as submitting by calling updateState({ navigation }) at lines 1906-1920 in packages/react-router/lib/router/router.ts. This state transition optionally aborts any ongoing load operations to prevent race conditions between data fetching and mutation requests.

Action Route Resolution and Execution

Fog-of-War Discovery for Lazy Routes

For applications utilizing code splitting and lazy-loaded routes, handleAction executes discoverRoutes (lines 2224-2240) to load missing leaf routes before invoking the action. This ensures the router has the complete route tree available to locate the correct action handler, even for dynamically imported route modules.

Target Route Selection

The router locates the action target using getTargetMatch(matches, location) at lines 7010-7020 in packages/react-router/lib/router/router.ts. This function selects the deepest path-contributing match or an index route that will receive the form data, ensuring nested route layouts don't accidentally intercept actions intended for deeper leaf nodes.

Action Function Invocation

Once the target route is identified, the router validates that an action function exists. If the route lacks an action (or lazy loader), the router returns a 405 Method Not Allowed error result (lines 7980-7990). Otherwise, it constructs a data-strategy payload via callDataStrategy (lines 7998-8015) that executes the user-provided action function and captures its DataResult.

Processing Action Results

Handling Redirects

If the action returns a redirect response, detected via isRedirectResult, the router immediately starts a redirect navigation and short-circuits the current flow (lines 8224-8245). This prevents unnecessary loader revalidation after a successful mutation that changes the URL.

Error Boundary Integration

When an action throws or returns an error result (isErrorResult), the router records a pending error on the nearest error boundary and flips the navigation type to Push at lines 8260-8280. This allows users to navigate back and retry the failed submission, preserving the form state in the browser history stack.

Success State and useActionData

For successful data results, the router returns { pendingActionResult: [routeId, result] } (lines 8300-8310), which is later merged into the router state and exposed via the useActionData() hook. This makes action results available to components for displaying success messages or validation errors.

Security and State Management

CSRF Protection

Before executing any action, the router validates the request origin through throwIfPotentialCSRFAttack in packages/react-router/lib/actions.ts (lines 1-38). This function compares the origin header against host / x-forwarded-host and any configured allowedActionOrigins, rejecting cross-site request forgery attempts before they reach application code.

After action execution (and subsequent loader revalidation unless short-circuited), completeNavigation finalizes the transition at lines 9660-9670 in packages/react-router/lib/router/router.ts. This updates state.actionData with the action result, clears the submitting state, and triggers a UI re-render with the mutated data.

Practical Implementation Example

// src/routes/contact.tsx
export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  await saveMessage(formData.get("message"));
  // redirect after successful mutation
  return redirect("/thanks");
}

// In the component
export default function Contact() {
  const actionData = useActionData(); // receives the result of the action
  const transition = useTransition(); // tells you if the form is submitting

  return (
    <Form method="post" action="/contact">
      <textarea name="message" required />
      <button type="submit" disabled={transition.state === "submitting"}>
        {transition.state === "submitting" ? "Sending…" : "Send"}
      </button>
      {actionData?.error && <p className="error">{actionData.error}</p>}
    </Form>
  );
}

The Form component automatically creates a Submission object, the router detects the POST method as a mutation, runs the exported action, and then either redirects or provides the result via useActionData().

Summary

  • React Router detects mutations via isMutationMethod, routing POST/PUT/PATCH/DELETE to handleAction instead of standard loaders
  • The pipeline includes fog-of-war discovery for lazy routes and getTargetMatch for precise target selection
  • Actions execute through callDataStrategy, with automatic 405 errors when routes lack action exports
  • Results are categorized as redirects (immediate navigation), errors (boundary capture), or success (useActionData)
  • CSRF protection runs automatically via throwIfPotentialCSRFAttack before any action code executes
  • State updates finalize through completeNavigation, committing results to state.actionData and re-rendering the UI

Frequently Asked Questions

What HTTP methods trigger React Router form actions?

Methods defined as mutations—POST, PUT, PATCH, and DELETE—trigger the action pipeline. GET submissions bypass actions and proceed to standard data loading, as determined by the isMutationMethod check in the router source.

How does React Router handle form actions on lazy-loaded routes?

The router runs discoverRoutes during handleAction (lines 2224-2240) to fetch missing route definitions before executing the action. This ensures code-split routes can handle form submissions without requiring eager loading of all route modules.

What happens if a route doesn't export an action function?

The router returns a 405 Method Not Allowed error result immediately (lines 7980-7990), preventing the submission from proceeding. This safety check ensures that only routes explicitly designed to handle mutations receive form data.

How is CSRF protection implemented for React Router form actions?

The throwIfPotentialCSRFAttack function in packages/react-router/lib/actions.ts validates the request origin header against the host or x-forwarded-host headers and any configured allowedActionOrigins. Invalid origins are rejected before the action function executes, preventing cross-site request forgery attacks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →