A practical, real-world guide to loaders, actions, fetchers, nested routes, revalidation & more.
Modern React applications need more than just routingβthey need data loading, mutations, caching, redirects, pagination, filters, streaming, and error handling.
React Router v6.4+ introduced the Data APIs, turning React Router into a powerful client-side framework similar to Remix or Next.js (but much simpler).
This guide walks you through everything step by step, from setup to advanced patterns.
β Table of Contents
- Why Data APIs?
- Project Setup (Vite + RouterProvider)
- Folder Structure
- Creating the Router
- Route Objects
- Loaders
- Actions
- Deferred Data (
defer) - Error Boundaries
- Navigation APIs
- Fetchers
- Data Caching & Revalidation
- Nested Routes & Layout Patterns
- Redirects & Search Params
- Final Thoughts
π― 1. Why React Router Data APIs?
Before v6.4, React Router only handled navigation.
You had to manually manage:
-
useEffectdata fetching -
useStateor Redux - Loading states
- Error handling
- Form submissions
- Navigation states
This meant boilerplate everywhere.
Data APIs solve this:
- Loaders β fetch data before rendering
- Actions β run POST/PUT/DELETE mutations
- Fetchers β inline form actions without navigation
- Navigation state β built-in loading UI
- Error boundaries β per route
-
Streaming (
defer) β faster UIs - Route-based caching β fewer network calls
React Router becomes both a router and a data layer.
π οΈ 2. Setup: Installing React Router (Vite + RouterProvider)
Create a Vite + React project:
npm create vite@latest my-app --template react
cd my-app
npm install react-router-dom
Now add your router:
π 3. Recommended Folder Structure
src/
main.jsx
router.jsx
layouts/
RootLayout.jsx
pages/
Home.jsx
Dashboard.jsx
errors/
RootError.jsx
services/
api.js
store/
useAppStore.js
This scales well for real-world applications.
π 4. Creating The Router (router.jsx)
Hereβs a minimal production-ready router using route objects:
import { createBrowserRouter } from "react-router-dom";
import RootLayout from "./layouts/RootLayout";
import RootError from "./errors/RootError";
import Home, { loader as homeLoader } from "./pages/Home";
export const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <RootError />,
children: [
{
index: true,
element: <Home />,
loader: homeLoader,
},
{
path: "dashboard",
lazy: async () => {
const module = await import("./pages/Dashboard.jsx");
return {
Component: module.default,
loader: module.loader,
};
}
}
]
}
]);
π 5. Mounting The Router (RouterProvider)
React Router Data APIs use RouterProvider, not BrowserRouter.
Update main.jsx:
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
β No nested <Routes> needed
β Router loads data automatically
β Handles errors, loaders, lazy routes
π§± 6. Layout Setup (Root Layout)
Your root layout should include:
- header/navigation
- global loader logic
-
<Outlet />for child routes
import { Outlet, NavLink, useNavigation } from "react-router-dom";
export default function RootLayout() {
const navigation = useNavigation();
const isLoading = navigation.state === "loading";
return (
<div>
<nav>
<NavLink to="/">Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
</nav>
{isLoading && <div className="loader">Loading...</div>}
<Outlet />
</div>
);
}
π₯ 7. Route Objects β The Core of Data API
Route objects allow:
- loaders
- actions
- lazy loaded components
- nested routes
- per-route error boundaries
Example:
{
path: "users",
element: <UsersPage />,
loader: usersLoader,
action: usersAction,
errorElement: <UsersError />
}
π¦ 8. Loaders β Fetch Data Before Rendering
Loaders run before a routeβs component mounts.
export async function loader() {
const res = await fetch("/api/messages");
return res.json();
}
Use inside component:
const data = useLoaderData();
Benefits:
- No flicker
- No useEffect needed
- Built-in caching
- Auto revalidation after actions
π 9. Actions β Handle Form Submissions
Actions run on:
<Form method="post">-
fetcher.submit()
export async function action({ request }) {
const form = await request.formData();
const name = form.get("name");
await fetch("/api/users", {
method: "POST",
body: JSON.stringify({ name })
});
return redirect("/users");
}
β‘ 10. Deferred Data (defer) β Faster UIs
Perfect for dashboards, heavy reports, analytics pages.
import { defer } from "react-router-dom";
export function loader() {
return defer({
fast: fetch("/api/summary").then(r => r.json()),
heavy: fetch("/api/big").then(r => r.json())
});
}
Use with <Await>:
<Suspense fallback="Loading...">
<Await resolve={data.fast}>{renderFast}</Await>
</Suspense>
<Suspense fallback="Loading big...">
<Await resolve={data.heavy}>{renderHeavy}</Await>
</Suspense>
β 11. Error Boundaries β Per Route
Each route gets its own error UI:
{
path: "users",
errorElement: <UsersError />
}
Use:
const err = useRouteError();
Prevents global crashes.
π¦ 12. Navigation APIs (useNavigation)
const navigation = useNavigation();
navigation.state; // "idle" | "loading" | "submitting"
Usecases:
- disable buttons
- show skeletons
- parent layout loaders
π 13. Fetchers β Inline Mutations Without Navigation
Perfect for:
- like buttons
- toggles
- delete actions
- row-level edits
const fetcher = useFetcher();
<fetcher.Form method="post" action="/users/123/delete">
<button>
{fetcher.state === "submitting" ? "Deleting..." : "Delete"}
</button>
</fetcher.Form>
Child page stays mounted β only data updates.
π 14. Data Caching & Revalidation
Loaders automatically re-run when:
- Navigation to same page
- Search params change
- Actions complete
-
revalidate()called - Same route path, different params
Loaders DO NOT re-run for parent routes unless needed.
π§± 15. Nested Routes & Layout Patterns
Organize UI like real apps:
RootLayout
βββ DashboardLayout
βββ ReportsLayout
βββ List
βββ Details
Parent stays mounted β child shows loader.
This gives fast, smooth UX.
π 16. Redirects & Search Params
redirect()
throw redirect("/login");
Search Params:
const [search, setSearch] = useSearchParams();
search.get("type");
setSearch({ type: "filtered" });
Ideal for:
- filters
- sorting
- pagination
- tabs
π Final Thoughts
React Router Data APIs give you:
- server-style data loading
- easy mutations
- caching & auto revalidation
- nested layouts
- streaming UIs
- built-in loading states
- per-route errors
- intuitive search param handling
- zero
useEffectdata fetching
This transforms React into a structured, predictable, scalable system for building real web applications.
Top comments (0)