In React Router v7, what is the difference between rendering <BrowserRouter> with <Routes> and creating a router with createBrowserRouter and <RouterProvider>?
answer
- when the router learns the routes
- render time versus creation time
- data hooks need a data router
- loader prop silently ignored
basics
~20 s<BrowserRouter> with <Routes> is declarative mode: routes are matched while React renders, giving URL matching and navigation only. createBrowserRouter builds a data router outside React, which alone runs loaders and actions, tracks pending navigations and renders route error boundaries.
solid answer
~40 sBoth use the same matcher, `<Link>` and `useNavigate`; the difference is *when the router learns the route table*. Under `<BrowserRouter>`, `<Routes>` turns its `<Route>` children into route objects during render, so the router cannot act before components run — a `loader` prop there is simply never called. `createBrowserRouter` receives the whole tree up front, so on every navigation it can run matched loaders in parallel before rendering, handle `action` submissions, expose `useNavigation` and fetchers, and catch errors into `errorElement`. Hooks such as `useLoaderData` throw "must be used within a data router" under `<BrowserRouter>`. The data router must be created once at module scope and passed to `<RouterProvider>`, imported from `react-router/dom` in a browser app. JSX is not the deciding factor: `createRoutesFromElements` gives a data router JSX definitions too.
code
tsx · 22 linesimport { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import { createRoot } from "react-dom/client";
import Root from "./Root";
import Home from "./Home";
import Projects, { projectsLoader } from "./Projects";
// Created once, at module scope - never inside a component.
const router = createBrowserRouter([
{
path: "/",
Component: Root,
children: [
{ index: true, Component: Home },
{ path: "projects", Component: Projects, loader: projectsLoader },
],
},
]);
createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />,
);go deeper
Recall the two setups: BrowserRouter with Routes versus createBrowserRouter with RouterProvider, and that only the second runs loaders and actions.
Explain why: declarative routes are only known during render, while a data router knows the table before rendering, so it can fetch, track pending state and catch route errors.
Show judgement about where data lives: pick declarative mode when a query cache owns data, data mode when the router should fetch before render, and create the router once at module scope.
Frame the mode choice as an ownership decision for a large app: which layer owns fetching, pending UI and errors, and what a later move to data or framework mode would cost each team.
## Two top-level router APIs React Router 7 can be used in three **modes**, and the mode is decided purely by which top-level router you render. This question is about the two that run as plain client-side libraries: **declarative mode** and **data mode**. Framework mode wraps data mode in a Vite plugin and is a separate topic. - **Declarative mode** — render `<BrowserRouter>` near the root and describe routes as JSX with `<Routes>` and `<Route>` wherever you need them. - **Data mode** — call `createBrowserRouter(routes)` once with an array of **route objects**, then render `<RouterProvider router={router} />`. Both modes share the same matching and ranking code and the same navigation primitives: `<Link>`, `useNavigate`, `useLocation`, `useParams`. What differs is *when the router learns the route table*, and every other difference follows from that single fact. ## Declarative mode: routes are discovered while React renders With `<BrowserRouter>`, the router only knows about a `<Routes>` block when React renders it. `<Routes>` converts its `<Route>` children into route objects and picks the best match for the current location, all inside render. Consequences: 1. The router cannot do anything *before* rendering, because it does not know which routes exist until the components that contain them run. 2. Routes can be spread over many `<Routes>` blocks in different components (so-called descendant routes). 3. Fetching, pending indicators and error handling are yours to build — effects, a query library, your own error boundaries. A `loader` or `action` prop written on a `<Route>` inside `<Routes>` is copied onto the route object and then **never called**. There is no error and no warning: nothing in declarative mode runs route data functions. This is a common source of confusion when someone pastes data-router examples into a `<BrowserRouter>` app. ## Data mode: the whole route table is known up front `createBrowserRouter` receives the full route tree *outside* React. When a navigation starts, the router already knows every matching route, so it can: - run every matched `loader` in parallel **before** it renders the next screen, - submit to an `action` and then revalidate the page's loaders, - expose in-flight state through `useNavigation` and independent requests through fetchers, - catch thrown errors into the nearest `errorElement` / `ErrorBoundary`, - block navigations with `useBlocker` and load route code on demand through the `lazy` property. The hooks built on that state — `useLoaderData`, `useActionData`, `useNavigation`, `useMatches`, `useRouteError`, `useBlocker`, `useRevalidator` — throw an invariant, *"… must be used within a data router"*, when called under `<BrowserRouter>`. ## Side by side | | `<BrowserRouter>` + `<Routes>` | `createBrowserRouter` + `<RouterProvider>` | |---|---|---| | When routes are known | during render | at router creation, before render | | How routes are written | JSX `<Route>` elements | route objects, or JSX via `createRoutesFromElements` | | `loader` / `action` | ignored | run by the router | | Pending UI, fetchers, blocking | not available | available | | Route-level error boundaries | not available | `errorElement` / `ErrorBoundary` | ## Wiring RouterProvider correctly The API docs say a data router **should not be held in React state**: create it once, at module scope, outside the React tree, and pass it to `<RouterProvider>`. Creating it inside a component body builds a brand-new router — with brand-new navigation state and loader data — whenever that component renders, which shows up as refetch storms and lost pending state. In v7 the packages were consolidated: everything is imported from `react-router`, and `react-router-dom` survives only as a re-export for upgrades. `RouterProvider` has a DOM-specific entry, `react-router/dom`, that wires in `ReactDOM.flushSync`; the upgrade guide recommends the top-level `react-router` export of `RouterProvider` only for non-DOM contexts such as tests. ## A symptom worth recognising A classic support question reads: *"I added a loader and `useLoaderData` crashes with 'must be used within a data router'."* The app renders `<BrowserRouter>`. The loader prop was silently ignored, and the hook failed because no data router exists. The fix is not a different hook — it is creating a data router. ## How to answer "which would you choose?" Declarative mode is a sound choice when routing is purely URL-to-component and something else — a query cache, a local-first sync layer — already owns data and pending states. Data mode is the choice as soon as you want the router to fetch before render, handle mutations with revalidation, or show route-scoped errors. Do not choose on syntax: JSX route definitions work in both, because `createRoutesFromElements` converts `<Route>` elements into the route objects a data router needs.
- Can a team that likes JSX route definitions still use a data router?Yes. `createRoutesFromElements(<Route …>…</Route>)` converts JSX `<Route>` elements, including their `loader`, `action` and `errorElement` props, into route objects, and you pass the result to `createBrowserRouter`. The routes are still created once, outside rendering, so every data feature works; only the authoring syntax changes.
- Why is creating the router inside a component body a bug?A data router is a long-lived object holding the current location, navigation state, loader data and fetchers. Constructing it during render creates a new instance on every render of that component, discarding that state and re-initialising. The docs say to create it once outside the React tree and pass it to `<RouterProvider>`.
- In v7, where should a browser app import RouterProvider from?From `react-router/dom`. That entry wraps the core provider and passes `ReactDOM.flushSync`, which the router uses for navigations that opt into synchronous updates. The v7 upgrade guide recommends the top-level `react-router` export for non-DOM contexts such as unit tests.
saying these in an interview costs you the question
- A loader prop on a Route inside BrowserRouter runs before the element renders
- createBrowserRouter is only needed for server rendering
- Creating the router inside App on each render is the normal pattern
- You must choose declarative mode if you want JSX route definitions
- In v7 browser apps still need the react-router-dom package installed
- The two modes rank and match routes with different algorithms