Choosing the Right React Router API: Data Router vs Component‑Based Routing
Decide between React Router’s data router API and classic component routing. Compare loaders, actions, error handling, and parallel fetches. See a concrete implementation and how to validate the choice.
10 Jul 2026, 21:51 UTC

Problem & Decision
When starting a new React app you’ll almost always need to fetch data for a route and, in many cases, mutate that data. React Router 6.4+ introduced a data router API (createBrowserRouter, loaders, actions) that tightly couples routing with data fetching. The classic component‑based API (/) still works and is often used with external state libraries like React Query or SWR. The decision is: Should your app use the data router or stay with the declarative component routing?
Key constraints that influence the decision:
- Do you need per‑route data loading that runs before the component renders?
- Will you perform mutations that should automatically re‑validate the route’s data?
- Do you want route‑specific error boundaries without adding external error‑handling logic?
- Is your team comfortable coupling routing to the router’s data lifecycle, or do you prefer to keep data logic in separate libraries?
- What React Router version are you using? The data router API was introduced in v6.4 and is now the official recommendation.
Supported Options
The two patterns are:
| Pattern | Primary API | Data Fetching | Mutations | Error Handling | Typical Use Case |
|---|---|---|---|---|---|
| Data Router | createBrowserRouter + RouterProvider | Loader functions run before render in parallel | Action functions + auto‑revalidate loaders | errorElement + useRouteError per route | New apps needing route‑coupled data and mutation handling |
| Component Routing | BrowserRouter + Routes + Route | Data fetched inside components (e.g., useEffect, React Query) | Manual refetch or cache invalidation | Global error boundaries or custom logic only | Legacy codebases or apps that already use a server‑state library |
Trade‑offs
- Coupling vs Flexibility: Data routers bind data logic to the router; this reduces boilerplate but limits use of external libraries that manage their own cache.
- Parallelism: Loaders run in parallel, eliminating the "first render without data" flash common in component routing.
- Mutation Flow: Actions automatically trigger loader re‑validation, whereas component routing requires explicit refetch logic.
- Error Boundaries: Per‑route errorElement is available only in data routers.
- Learning Curve: Data routers introduce new concepts (loader, action, useNavigation) that might be unfamiliar to teams used to component‑based patterns.
- Version Compatibility: Data router APIs are stable from v6.4 onward; older projects on v6.3 or earlier cannot use them.
Concrete Implementation
Data Router Example
Below is a minimal data router setup that demonstrates a loader, an action, and an errorElement.
// src/router.js
import { createBrowserRouter, RouterProvider, redirect, json } from "react-router-dom";
import Home from "./components/Home";
import Edit from "./components/Edit";
import ErrorPage from "./components/ErrorPage";
const router = createBrowserRouter([
{
path: "/",
element: , // rendered after loader resolves
loader: async () => {
const res = await fetch("/api/items");
if (!res.ok) throw new Response("Failed to load items", { status: 500 });
return json(await res.json());
},
errorElement: ,
children: [
{
path: "edit/:id",
element: , // receives loader data via useLoaderData
loader: async ({ params }) => {
const res = await fetch(`/api/items/${params.id}`);
if (!res.ok) throw new Response("Not found", { status: 404 });
return json(await res.json());
},
action: async ({ request, params }) => {
const formData = await request.formData();
const body = Object.fromEntries(formData);
const res = await fetch(`/api/items/${params.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) return json({ error: "Update failed" }, { status: 400 });
// After mutation, re‑validate the loader automatically
return redirect("/");
},
errorElement: ,
},
],
},
]);
export default function App() {
return ;
}
In Edit you can use useLoaderData to get the item, useNavigation to show a pending state, and useRouteError inside ErrorPage to display route‑specific errors.
Component Routing Example
For comparison, here’s the same structure using the classic API and React Query for data fetching.
// src/App.js
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "react-query";
import Home from "./components/Home";
import Edit from "./components/Edit";
const queryClient = new QueryClient();
export default function App() {
return (
} />
} />
} />
);
}
Inside Edit, you’d use useQuery to fetch the item and useMutation to submit changes, then manually invalidate the query or refetch.
Validation & Verification
- Install the correct React Router version:
npm install react-router-dom@latest # verify package.json contains "react-router-dom": "^6.4.0" - Run the data router example and navigate to "/edit/1".
- Before the component mounts, the loader should fetch data; you can confirm by inspecting the network tab for the API call.
- Submitting the form should trigger the action and automatically re‑run the parent loader, updating the UI without manual refetch.
- Throwing an error in the loader (e.g., return a 500 response) should render
ErrorPageviaerrorElement.
- Run the component‑routing example and verify that data is fetched inside
EditviauseQuery.- After mutation, ensure you call
queryClient.invalidateQueries("item-1")orrefetch()to update the UI.
- After mutation, ensure you call
- Check that
useNavigationis only available in the data router pattern; attempting to use it in component routing will result in an error.
Limitations & Practical Checks
- Loaders are not a cache; each navigation re‑runs them unless you add your own caching layer (e.g.,
React QuerywithkeepPreviousData). - Mixing
<BrowserRouter>withcreateBrowserRouterwill silently fail; always useRouterProviderwith data routers. - When using the data router, avoid external server‑state libraries that maintain their own cache, unless you explicitly synchronize them with loader data.
- Version mismatches (e.g., using v6.3) will not recognize
createBrowserRouter; upgrade or use the component routing path. - For SSR, data routers provide
dehydratehelpers; component routing requires manual hydration logic.
By following the verification steps above, you can confidently decide which routing strategy aligns with your project’s data needs and team expertise.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.