Describe how React Router v6's data APIs (e.g., useLoaderData, useActionData) differ from prior versions and why they matter for data loading.
React Router v6 introduces declarative, route-level data loading and mutations via loaders and actions—decoupling data concerns from component logic and enabling built-in loading/error states, SSR readiness, and optimized client-side hydration.
Short Answer
React Router v6 replaces imperative, component-managed data fetching (e.g., useEffect + fetch in v5) with declarative, route-centric loader and action functions that run before rendering—enabling automatic loading/error UI, SSR support, and atomic data dependencies per route.
Details
In v5, data loading was entirely manual: developers fetched inside components (often in useEffect) and managed loading/error states themselves. v6 shifts this to the route configuration level—each route can define a loader (for read operations) and action (for mutations), both running on the server and client. These functions return resolved data (or throw errors), which is then consumed via useLoaderData() and useActionData(). Crucially, loaders run during navigation, before the route renders—so React Router can suspend or show pending states (via <Await> + Suspense), handle errors uniformly (via errorElement), and serialize data for SSR/hydration. This enforces separation of concerns, eliminates race conditions from stale useEffect fetches, and makes routes truly self-contained data units.
Example
// Route config with loader & action
const router = createBrowserRouter([
{
path: "/posts/:id",
element: <PostPage />,
errorElement: <ErrorBoundary />,
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.id}`);
if (!res.ok) throw new Response("Post not found", { status: 404 });
return res.json();
},
action: async ({ request }) => {
const formData = await request.formData();
await fetch("/api/posts", { method: "POST", body: formData });
return { success: true };
}
}
]);
// Component consumes pre-fetched data
function PostPage() {
const post = useLoaderData<typeof loader>(); // typed, guaranteed non-null
const actionData = useActionData<typeof action>();
return (
<div>
<h1>{post.title}</h1>
{actionData?.success && <p>✓ Saved!</p>}
</div>
);
}Bonus
To stand out: mention how v6’s data APIs integrate with frameworks like Remix and Next.js App Router (which inspired them); highlight that loader/action functions are serializable—no closures or React state—making them safe for SSR and caching; and note advanced patterns like defer() + <Await> for progressive loading of expensive data without blocking the entire route. Also emphasize that this model enables data-driven routing decisions: e.g., redirecting in a loader if auth fails (throw redirect("/login")).