skip to content

Route Configuration

Two ways to declare routes — JSX <Route> trees or the object-based createBrowserRouter — plus index routes, path syntax, and wiring it up through RouterProvider. Interviewers ask which you would choose and what the data APIs require you to use.

on this pageshow

explore

questions

5

In React Router v7, what is the difference between rendering <BrowserRouter> with <Routes> and creating a router with createBrowserRouter and <RouterProvider>?

level: juniorimportance: must knowfreq 72%

answer

  1. when the router learns the routes
  2. render time versus creation time
  3. data hooks need a data router
  4. 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 s

Both 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 lines
tsx
import { 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

for a junior

Recall the two setups: BrowserRouter with Routes versus createBrowserRouter with RouterProvider, and that only the second runs loaders and actions.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

In React Router, what does marking a route with index: true do, and at which URL does that route render?

level: juniorimportance: should knowfreq 55%

basics

~20 s

An index route (index: true, no path) is a parent's default child: it renders in the parent's <Outlet> when the URL matches the parent's path exactly, such as /dashboard. Index routes cannot have children of their own.

open as a page

In a React Router v7 data router, what does a route object's lazy property load, and which route properties can it never supply?

level: middleimportance: should knowfreq 38%

basics

~20 s

route.lazy loads a matched route's non-matching properties — Component, loader, action, ErrorBoundary and similar — on first navigation to it. It can never supply path, index, children, id or caseSensitive, because the router needs those to match before loading anything.

open as a page

In React Router, when both /teams/new and /teams/:teamId are defined, which route renders for /teams/new, and how does the router decide?

level: middleimportance: should knowfreq 48%

basics

~20 s

/teams/new renders the static route. React Router scores every candidate branch — static segments weigh most, dynamic segments less, a splat is penalised — and picks the highest score, so declaration order only breaks ties between siblings.

open as a page

You are moving a large React Router app from <BrowserRouter> with descendant <Routes> to createBrowserRouter; how do you migrate incrementally, and what silently breaks along the way?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Wrap the existing app in a data router with a single catch-all route (path '*') that renders the old <Routes>, then lift routes into route objects one at a time. Loaders placed on still-descendant routes silently never run.

open as a page