Data Mode
Data Mode adds powerful data loading and mutation features to React Router by moving route configuration outside of React rendering. This enables features like loaders, actions, pending states, and optimistic UI.Quick Start
Why Data Mode?
Data Mode is ideal when you:- Want data loading features but don’t want a full framework
- Need control over your bundler and server setup
- Are migrating from React Router v6.4+
- Want to integrate with existing build tools
Data Mode gives you the power of data loading without requiring the Vite plugin.
Route Configuration
Routes are configured as plain JavaScript objects:import { createBrowserRouter } from "react-router";
const router = createBrowserRouter([
{
path: "/",
Component: Root,
},
{
path: "/about",
Component: About,
},
{
path: "/teams/:teamId",
Component: Team,
},
]);
const router = createBrowserRouter([
{
path: "/",
Component: Root,
children: [
{
index: true,
Component: Home,
},
{
path: "dashboard",
Component: Dashboard,
children: [
{ index: true, Component: DashboardHome },
{ path: "settings", Component: Settings },
],
},
],
},
]);
const router = createBrowserRouter([
{
// No path = layout route
Component: MarketingLayout,
children: [
{ index: true, Component: Home },
{ path: "contact", Component: Contact },
],
},
{
path: "projects",
children: [
{ index: true, Component: ProjectsHome },
{
// Another layout route
Component: ProjectLayout,
children: [
{ path: ":pid", Component: Project },
{ path: ":pid/edit", Component: EditProject },
],
},
],
},
]);
Data Loading with Loaders
Loaders provide data to route components before they render:- Basic Loader
- With Request
- Error Handling
import { createBrowserRouter, useLoaderData } from "react-router";
const router = createBrowserRouter([
{
path: "/teams/:teamId",
loader: async ({ params }) => {
const team = await fetchTeam(params.teamId);
return { team };
},
Component: Team,
},
]);
function Team() {
const { team } = useLoaderData();
return <h1>{team.name}</h1>;
}
const router = createBrowserRouter([
{
path: "/products",
loader: async ({ request }) => {
const url = new URL(request.url);
const searchTerm = url.searchParams.get("q");
const products = await searchProducts(searchTerm);
return { products, searchTerm };
},
Component: Products,
},
]);
function Products() {
const { products, searchTerm } = useLoaderData();
return (
<div>
<h1>Search Results for "{searchTerm}"</h1>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
const router = createBrowserRouter([
{
path: "/user/:userId",
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
if (!user) {
throw new Response("Not Found", { status: 404 });
}
return { user };
},
Component: User,
errorElement: <ErrorBoundary />,
},
]);
function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.statusText}</p>
</div>
);
}
return <div>Something went wrong!</div>;
}
Data Mutations with Actions
Actions handle form submissions and data mutations:import { Form, redirect } from "react-router";
const router = createBrowserRouter([
{
path: "/projects/new",
action: async ({ request }) => {
const formData = await request.formData();
const project = await createProject({
name: formData.get("name"),
description: formData.get("description"),
});
return redirect(`/projects/${project.id}`);
},
Component: NewProject,
},
]);
function NewProject() {
return (
<Form method="post">
<input name="name" placeholder="Project name" />
<textarea name="description" />
<button type="submit">Create Project</button>
</Form>
);
}
const router = createBrowserRouter([
{
path: "/login",
action: async ({ request }) => {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const errors = {};
if (!email) errors.email = "Email is required";
if (!password) errors.password = "Password is required";
if (Object.keys(errors).length > 0) {
return { errors };
}
const user = await login(email, password);
return redirect("/dashboard");
},
Component: Login,
},
]);
function Login() {
const actionData = useActionData();
return (
<Form method="post">
<input name="email" />
{actionData?.errors?.email && <span>{actionData.errors.email}</span>}
<input name="password" type="password" />
{actionData?.errors?.password && <span>{actionData.errors.password}</span>}
<button type="submit">Login</button>
</Form>
);
}
const router = createBrowserRouter([
{
path: "/projects/:projectId",
loader: async ({ params }) => {
return { project: await fetchProject(params.projectId) };
},
Component: Project,
},
{
path: "/projects/:projectId/delete",
action: async ({ params }) => {
await deleteProject(params.projectId);
return redirect("/projects");
},
},
]);
function Project() {
const { project } = useLoaderData();
return (
<div>
<h1>{project.name}</h1>
<Form method="post" action="delete">
<button type="submit">Delete</button>
</Form>
</div>
);
}
Pending UI States
Show loading states during navigation and submissions:- Navigation State
- Form Submission
- NavLink Pending
import { useNavigation } from "react-router";
function Root() {
const navigation = useNavigation();
const isLoading = navigation.state === "loading";
return (
<div>
{isLoading && <div className="loading-bar" />}
<Outlet />
</div>
);
}
import { useNavigation, Form } from "react-router";
function EditProject() {
const { project } = useLoaderData();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input name="name" defaultValue={project.name} />
<button disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</button>
</Form>
);
}
import { NavLink } from "react-router";
function Navigation() {
return (
<nav>
<NavLink
to="/dashboard"
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
}
>
Dashboard
</NavLink>
</nav>
);
}
Fetchers for Parallel Mutations
UseuseFetcher to load data or submit forms without navigation:
import { useFetcher } from "react-router";
function NewsletterSignup() {
const fetcher = useFetcher();
const isSubscribing = fetcher.state === "submitting";
return (
<fetcher.Form method="post" action="/newsletter/subscribe">
<input name="email" type="email" />
<button disabled={isSubscribing}>
{isSubscribing ? "Subscribing..." : "Subscribe"}
</button>
{fetcher.data?.success && <p>Thanks for subscribing!</p>}
</fetcher.Form>
);
}
import { useFetcher } from "react-router";
function UserDetails({ userId }) {
const fetcher = useFetcher();
return (
<div>
{fetcher.state === "idle" && !fetcher.data && (
<button onClick={() => fetcher.load(`/users/${userId}`)}
Load Details
</button>
)}
{fetcher.state === "loading" && <div>Loading...</div>}
{fetcher.data && (
<div>
<h2>{fetcher.data.user.name}</h2>
<p>{fetcher.data.user.email}</p>
</div>
)}
</div>
);
}
import { useFetcher } from "react-router";
function Task({ task }) {
const fetcher = useFetcher();
const isComplete = fetcher.formData
? fetcher.formData.get("complete") === "true"
: task.complete;
return (
<div>
<fetcher.Form method="post">
<input
type="hidden"
name="complete"
value={isComplete ? "false" : "true"}
/>
<button type="submit">
{isComplete ? "✓" : "○"} {task.title}
</button>
</fetcher.Form>
</div>
);
}
Advanced Patterns
Comparison with Framework Mode
| Feature | Data Mode | Framework Mode |
|---|---|---|
| Loaders | ✅ Manual types | ✅ Auto-generated types |
| Actions | ✅ | ✅ |
| Code Splitting | Manual | Automatic |
| SSR | Manual setup | Built-in |
| Bundler | Your choice | Vite required |
| File-based routing | No | Yes (optional) |
Data Mode gives you full control over your build process while still providing powerful data loading features.