Skip to main content

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

1
Install React Router
2
npm install react-router
3
Create a router
4
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} />
);
5
Start your dev server
6
npm run dev

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,
  },
]);

Data Loading with Loaders

Loaders provide data to route components before they render:
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>;
}

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>
  );
}

Pending UI States

Show loading states during navigation and submissions:

Fetchers for Parallel Mutations

Use useFetcher 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>
  );
}

Advanced Patterns

1
Route Context
2
Share data across route loaders:
3
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);
        },
      },
    ],
  },
]);
4
Deferred Data
5
Stream slow data after initial render:
6
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>
  );
}
7
Scroll Restoration
8
import { ScrollRestoration } from "react-router";

function Root() {
  return (
    <div>
      <Outlet />
      <ScrollRestoration />
    </div>
  );
}

Comparison with Framework Mode

FeatureData ModeFramework Mode
Loaders✅ Manual types✅ Auto-generated types
Actions
Code SplittingManualAutomatic
SSRManual setupBuilt-in
BundlerYour choiceVite required
File-based routingNoYes (optional)
Data Mode gives you full control over your build process while still providing powerful data loading features.

Next Steps

1
Add Error Boundaries
2
Handle errors gracefully with errorElement on routes.
3
Implement Authentication
4
Use loaders to protect routes and redirect unauthenticated users.
5
Optimize with Prefetching
6
Use <Link prefetch> to load data before navigation.

Build docs developers (and LLMs) love