React & Modern Frontend (445 Qs)•Part 10: Modern React 18, React 19 & Next.js Patterns (317 - 353)
What are loaders, actions, and data APIs in modern React Router?
Starting with React Router v6.4, the library ships data APIs (createBrowserRouter, loader, action, useLoaderData, useActionData, useFetcher) that move data fetching and mutations out of components and into the route definitions themselves — conceptually similar to Next.js server components/actions, but framework-agnostic.
- Loader: a function attached to a route that fetches the data that route needs before (or in parallel with) rendering, eliminating loading-state waterfalls ("render-then-fetch").
- Action: a function attached to a route that handles data mutations (form submissions — create/update/delete), typically triggered via
<Form>oruseFetcher(). useLoaderData()/useActionData(): hooks that read the data returned by the matching route'sloader/actioninside the rendered component.
jsx
import { createBrowserRouter, RouterProvider, useLoaderData, Form, redirect } from "react-router-dom";
// Loader: runs before the route renders, its return value is available via useLoaderData()
async function userLoader({ params }) {
const res = await fetch(`/api/users/${params.userId}`);
if (!res.ok) throw new Response("Not Found", { status: 404 });
return res.json();
}
// Action: runs when a <Form> on this route is submitted
async function updateUserAction({ request, params }) {
const formData = await request.formData();
await fetch(`/api/users/${params.userId}`, { method: "PUT", body: formData });
return redirect(`/users/${params.userId}`); // navigate after a successful mutation
}
function UserProfile() {
const user = useLoaderData(); // already-fetched data, no useEffect needed
return (
<Form method="put">
<input name="name" defaultValue={user.name} />
<button type="submit">Save</button>
</Form>
);
}
const router = createBrowserRouter([
{
path: "/users/:userId",
element: <UserProfile />,
loader: userLoader,
action: updateUserAction,
errorElement: <ErrorPage />,
},
]);
function App() {
return <RouterProvider router={router} />;
}
Key points:
- Loaders run before the route component renders, so data is ready immediately — no
useEffect+isLoadingflicker for the initial fetch. - Errors thrown inside a
loader/action(including thrownResponses for status codes) are caught by the nearest route'serrorElement, unifying error handling for fetch failures and render errors. useFetcher()lets you call loaders/actions without navigating (e.g., optimistic "like" buttons, inline edits in a list).- This pattern requires the data-router setup (
createBrowserRouter+<RouterProvider>) instead of the plain<BrowserRouter>/<Routes>components — it's opt-in, existing<Routes>-based apps keep working unchanged.