When we originally began bringing the Remix Data APIs over to React Router we realized that they brought about a pretty different way of structuring your routes. Instead of discovering routes via the <Routes>
component as React rendered the component tree, we needed to lift the route definitions so we could decouple fetching from rendering.
This brought about an interesting conundrum. We've got tons of v6 BrowserRouter
apps out there happily defining their routes via <Routes>
components -- how can we provide them with a smooth upgrade experience that doesn't require a big-bang migration to the new approach? This ruled out a new major version and we focused on adding these new features in a fully backwards compatible way that would provide users an incremental upgrade path from BrowserRouter
to RouterProvider
.
The first thing to be aware of is the presence of a handful of new Data APIs that only work on routes defined via the new data routers (i.e., createBrowserRouter
). These include a few categories of APIs:
loader
, action
, shouldRevalidate
, handle
, and lazy
useLoaderData
, useActionData
, useFetcher
, useMatches
, useNavigation
, etc.route.errorElement
, route.ErrorBoundary
, and useRouteError
The rest of the APIs that existed prior to v6.4.0 are still usable in both BrowserRouter
and RouterProvider
apps. These include common hooks/components such as useNavigate
, useLocation
, useParams
, <Link>
, <Outlet />
, etc.
We built the new <RouterProvider>
component such that it would enable the new Data APIs on routes defined at the root router, while not excluding descendant <Routes>
trees so commonly used in BrowserRouter
apps. This was explicitly to allow incremental migration from one to the other. Let's take a look at how we would do this.
Let's assume we've got a current application with 2 descendant route trees, and assume these routes are all doing in-component data fetching, and rendering their own loading and error states.
import {
BrowserRouter,
Link,
Route,
Routes,
} from "react-router-dom";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
</BrowserRouter>
);
}
function Home() {
return (
<>
<h1>Welcome!</h1>
<p>
Check out the <Link to="/blog">blog</Link> or the{" "}
<Link to="users">users</Link> section
</p>
</>
);
}
function BlogApp() {
return (
<Routes>
<Route index element={<h1>Blog Index</h1>} />
<Route path="posts" element={<h1>Blog Posts</h1>} />
</Routes>
);
}
function UserApp() {
return (
<Routes>
<Route index element={<h1>Users Index</h1>} />
</Routes>
);
}
We can render this application inside a RouterProvider
with only a few small changes:
App
component to Root
<BrowserRouter>
componentRoot
elementApp
component rendering a <RouterProvider>
import {
createBrowserRouter,
Link,
Route,
RouterProvider,
Routes,
} from "react-router-dom";
// 3️⃣ Router singleton created
const router = createBrowserRouter([
{ path: "*", Component: Root },
]);
// 4️⃣ RouterProvider added
export default function App() {
return <RouterProvider router={router} />;
}
// 1️⃣ Changed from App to Root
function Root() {
// 2️⃣ `BrowserRouter` component removed, but the <Routes>/<Route>
// component below are unchanged
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
);
}
function Home() {
/* Unchanged */
}
function BlogApp() {
/* Unchanged */
}
function UserApp() {
/* Unchanged */
}
🥳 Congrats - you're now rendering a data router app! But wait a minute - we can't use any of the new stuff yet since none of our routes are defined at the top with createBrowserRouter
😢. To access the new APIs, we need to start lifting routes one-by-one to the data router.
Let's start with the /
route for the <Home>
component. All we need to do is lift the <Route>
definition up to the data router:
const router = createBrowserRouter([
{ path: "/", Component: Home }, // 🆕
{ path: "*", Component: Root },
]);
export default function App() {
return <RouterProvider router={router} />;
}
function Root() {
return (
<Routes>
{/* ⬆️ Home route lifted up to the data router */}
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
);
}
Now you can add data APIs to your home route (loader
, action
, errorElement
) and start leveraging data hooks inside your Home component (useLoaderData
, useActionData
, useFetcher
, <Form>
, etc.).
Now let's look at lifting the Blog App upwards, but still doing it one leaf route at a time. In order to lift the /blog
index route up, we need the /blog/*
splat route lifted as well, but we can still render the /blog/posts
route where it is and do that separately.
const router = createBrowserRouter([
{ path: "/", Component: Home },
{
// Lifted blog splat route
path: "/blog/*",
children: [
// New blog index route
{ index: true, Component: () => <h1>Blog Index</h1> },
// Blog subapp splat route added for /blog/posts matching
{ path: "*", Component: BlogApp },
],
},
{ path: "*", Component: Root },
]);
export default function App() {
return <RouterProvider router={router} />;
}
function Root() {
return (
<Routes>
{/* ⬆️ Blog splat route lifted */}
<Route path="/users/*" element={<UserApp />} />
</Routes>
);
}
function BlogApp() {
return (
<Routes>
{/* ⬆️ Blog index route lifted */}
<Route path="posts" element={<h1>Blog Posts</h1>} />
</Routes>
);
}
And now your blog index route can participate in data loading.
You can keep doing this one route at a time until you've eventually converted all of your routes to data routes and can no longer use any nested <Routes>
to define your routing tree. To avoid bundle bloat, it's recommended to leverage the route.lazy prop to lazily load your routes.
<BrowserRouter>
and <Routes>
Many folks render an app shell around their <Routes>
via something like the following:
export default function App() {
return (
<BrowserRouter>
<header>
<h1>My Super Cool App</h1>
<NavMenu />
</header>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Routes>
</main>
<footer>©️ me 2023</footer>
</BrowserRouter>
);
}
If you find yourself in this situation, don't worry - there's a straightforward solution you can do before starting the above migration.
This is quite common but poses a problem in the above migration approach since we need to lift things to RouterProvider
route-by-route, but this "app shell" stuff isn't part of a route...but it could be! That "app shell" is really nothing more than a layout route with an <Outlet>
! So before starting the above migration, just move this "app shell" into a pathless layout route around your routes as follows:
export default function App() {
return (
<BrowserRouter>
<Routes>
{/* 1️⃣ Wrap your routes in a pathless layout route */}
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/blog/*" element={<BlogApp />} />
<Route path="/users/*" element={<UserApp />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Layout() {
return (
<>
<header>
<h1>My Super Cool App</h1>
<NavMenu />
</header>
<main>
{/* 2️⃣ Render the app routes via the Layout Outlet */}
<Outlet />
</main>
<footer>©️ me 2023</footer>
</>
);
}
Once you've done that, you can proceed with the above migration strategy and start lifting routes into your RouterProvider
one-by-one. You'll likely want to lift the layout route first so all of the children can nest inside of it.