React Router loaders and actions are the data mode APIs for fetching and mutating route data. A loader fetches the data a route needs before it renders, and an action handles form submissions. They run through createBrowserRouter, the data mode alternative to the declarative BrowserRouter setup.
Set up a data router
Data mode builds a router object instead of JSX routes. Create it once outside the component tree and pass it to RouterProvider.
import { createRoot } from "react-dom/client";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import { routes } from "./routes.jsx";
const router = createBrowserRouter(routes);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);RouterProvider comes from react-router/dom, which wires up React DOM's flushSync for browser apps. The routes file holds the route objects, including their loaders and actions.
This is the data mode setup, distinct from the declarative BrowserRouter and Routes components. Data mode is what unlocks loaders and actions.
Fetch data with a loader
A loader is an async function on a route object. It receives the route params and returns data, which the component reads with useLoaderData.
import { fetchTeam } from "./api.js";
export const routes = [
{
path: "/teams/:teamId",
loader: async ({ params }) => {
const team = await fetchTeam(params.teamId);
return { name: team.name };
},
Component: Team,
},
];The Team component reads that data after the loader resolves. It calls useLoaderData, which returns the object the loader resolved with.
import { useLoaderData } from "react-router";
export default function Team() {
const data = useLoaderData();
return <h1>{data.name}</h1>;
}React Router waits for the loader before rendering Team, so the page never renders with missing data. While the loader runs, the route is in a loading state, which you can surface with useNavigation.
The loader returns a plain object, and useLoaderData hands that object back to the component. TypeScript can infer the shape from the loader's return type.
Mutate data with an action
An action handles a form submission. It reads the submitted form data from the request and returns a result.
{
path: "/teams/:teamId/edit",
action: async ({ request, params }) => {
const formData = await request.formData();
await updateTeam(params.teamId, formData.get("name"));
return { ok: true };
},
Component: EditTeam,
}The action receives the route params and a request that contains the submitted form values. It returns any data the page should show after the submission.
Submit with Form and read the result
The Form component posts to the route's action without a page reload. After the action runs, React Router revalidates the loaders on the page.
import { Form, useActionData } from "react-router";
export default function EditTeam() {
const data = useActionData();
return (
<Form method="post">
<input name="name" aria-label="Team name" />
<button type="submit">Save</button>
{data?.ok ? <p>Saved</p> : null}
</Form>
);
}useActionData returns the action's result after submission, or undefined before one has run. The input gets an accessible name through its aria-label.
After the action resolves, React Router revalidates the loaders on the page by default. A list updated by the action reflects the change without a manual refetch.
While a submission runs, useNavigation reports the state as "submitting", so you can disable the button and prevent duplicate posts.
import { useNavigation } from "react-router";
const navigation = useNavigation();
const busy = navigation.state === "submitting";Pass busy to the button's disabled prop, or show a spinner. Loading, empty, success, and error states are covered in how to handle loading, error, empty, and success states.
Redirect from a loader
When a route requires authentication, the loader can throw a redirect instead of returning data.
import { redirect } from "react-router";
loader: async ({ request }) => {
const user = await getUser(request);
if (!user) throw redirect("/login");
return { user };
},redirect returns a Response with a 302 status, and the router sends the browser to /login. This is the data mode version of the guard component in protected routes in React.
Throwing redirect from a loader runs before the route renders, so a signed-out user never sees protected content.
Rune AI
Key Insights
- Create a data router with createBrowserRouter and RouterProvider.
- Return data from a loader and read it with useLoaderData.
- Handle submissions in an action and read the result with useActionData.
- Throw redirect from a loader to guard a route.
Frequently Asked Questions
Do loaders run on the server or client?
When does an action revalidate loaders?
How do I redirect from a loader?
Conclusion
Loaders fetch a route's data before render, and actions handle form mutations with automatic revalidation. Set up data mode with createBrowserRouter and RouterProvider, read results with useLoaderData and useActionData, and throw redirect for auth checks.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.