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.
Navigation Completion
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 tohandleActioninstead of standard loaders - The pipeline includes fog-of-war discovery for lazy routes and
getTargetMatchfor 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
throwIfPotentialCSRFAttackbefore any action code executes - State updates finalize through
completeNavigation, committing results tostate.actionDataand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →