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
import { createBrowserRouter , RouterProvider } from "react-router" ;
import { createRoot } from "react-dom/client" ;
const router = createBrowserRouter ([
{
path: "/" ,
element: < div > Hello World </ div > ,
},
]);
createRoot ( document . getElementById ( "root" )). render (
< RouterProvider router = { router } />
);
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:
Basic
Nested Routes
Layout Routes
import { createBrowserRouter } from "react-router" ;
const router = createBrowserRouter ([
{
path: "/" ,
Component: Root ,
},
{
path: "/about" ,
Component: About ,
},
{
path: "/teams/:teamId" ,
Component: Team ,
},
]);
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:
Basic Action
With Validation
Delete Action
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 >
);
}
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
Use useFetcher to load data or submit forms without navigation:
Newsletter Signup
Load on Demand
Optimistic UI
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 >
);
}
Advanced Patterns
Share data across route loaders:
const router = createBrowserRouter ([
{
id: "root" ,
path: "/" ,
loader : async () => {
return { user: await getCurrentUser () };
},
Component: Root ,
children: [
{
path: "dashboard" ,
loader : () => {
// Access parent data
const { user } = useRouteLoaderData ( "root" );
return loadDashboard ( user . id );
},
},
],
},
]);
Stream slow data after initial render:
import { defer , Await } from "react-router" ;
import { Suspense } from "react" ;
const router = createBrowserRouter ([
{
path: "/product/:id" ,
loader : async ({ params }) => {
const product = await fetchProduct ( params . id );
const reviews = fetchReviews ( params . id ); // Don't await!
return defer ({ product , reviews });
},
Component: Product ,
},
]);
function Product () {
const { product , reviews } = useLoaderData ();
return (
< div >
< h1 > { product . name } </ h1 >
< Suspense fallback = { < ReviewsSkeleton /> } >
< Await resolve = { reviews } >
{ ( resolvedReviews ) => < Reviews items = { resolvedReviews } /> }
</ Await >
</ Suspense >
</ div >
);
}
import { ScrollRestoration } from "react-router" ;
function Root () {
return (
< div >
< Outlet />
< ScrollRestoration />
</ div >
);
}
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.
Next Steps
Handle errors gracefully with errorElement on routes.
Use loaders to protect routes and redirect unauthenticated users.
Optimize with Prefetching
Use <Link prefetch> to load data before navigation.