File-based routing with React Router — Upgrading to v6

Upgrading file-based routing setup to the newly released React Router v6

Cowritten with Marian Molina December 24, 2021

⌁ Check out Generouted if you want to use client-side file-based routing. It provides more features than what’s covered here, multiple frameworks support and multiple routers integrations!


In this post we’ll update the file-based routing setup with React Router v6. In the previous three posts we talked about how to setup file-based routing for client-side React applications with React Router and Vite. We covered the base setup for file-based routing then added route-based code-splitting to enable on-demand loading. Finally we added route-based pre-loading with a custom Link component.

Upgrading to React Router v6 is straight forward. React Router provides a comprehensive upgrade guide to v6, I highly recommend checking it out if you want to have more information about the released updates and React Router in general.

Let’s first list the updates we’ll be doing to the previous setup in few points:

Upgrading React Router

We can simply run an npm installation command with the target version, v6 in this case.

npm install react-router-dom@6

Also we can remove the installed @types/react-router-dom package for the previous React Router versions, as now react-router-dom v6 comes with built-in TypeScript declarations.

npm remove @types/react-router-dom

Adding <Routes>

React Router introduced a new <Routes> component that replaces the <Switch> component for defining routes. It offers a couple of advantages over the <Switch> component that has mainly to do with routes paths matching and routes nesting.

// src/config/routes.tsx import { Routes, Route } from 'react-router-dom' // ... export const Routes = (): JSX.Element => { const App = preserved?.['_app'] || Fragment const NotFound = preserved?.['404'] || Fragment return ( <App> <Suspense fallback={'Loading...'}> <Routes>{/* ... */}</Routes> </Suspense> </App> ) }

Updating <Route>

The main change in the <Route> component is route rendering, the <Route children> is now reserved for the nested routes. So the <Route children> or <Route component> rendering is now moved to the <Route element>.

There are also some advantages of the new <Route element> rendering covered in React Router docs.

Here is an example of this change for the <Route children>. Instead of passing the component to be rendered as a child within the <Route><Route><Component /></Route>. We now pass it to the <Route element> prop → <Route element={<Component />} />:

// src/config/routes.tsx // ... export const Routes = (): JSX.Element => { const App = preserved?.['_app'] || Fragment const NotFound = preserved?.['404'] || Fragment return ( <App> <Suspense fallback={'Loading...'}> <Routes> {routes.map(({ path, component: Component = Fragment }) => ( <Route key={path} path={path} element={<Component />} /> ))} <Route path="*" element={<NotFound />} /> </Routes> </Suspense> </App> ) }

Using <Navigate>

We used to have a <Redirect> component in order to handle routes redirecting. It has been removed from React Router v6. Here are some notes on handling redirects in React Router v6 in both client-side and server-side.

We can have a similar behavior for client-side redirects using the newly added <Navigate> component:

import { Navigate } from 'react-router-dom' // <Redirect to="/login" /> // v5 // <Navigate to="/login" replace /> // v6

Final code

// src/routes.tsx import { Fragment, lazy, Suspense } from 'react' import { Routes as BrowserRoutes, Route } from 'react-router-dom' const PRESERVED = import.meta.globEager('/src/pages/(_app|404).tsx') const ROUTES = import.meta.glob('/src/pages/**/[a-z[]*.tsx') const preserved = Object.keys(PRESERVED).reduce((preserved, file) => { const key = file.replace(/\/src\/pages\/|\.tsx$/g, '') return { ...preserved, [key]: PRESERVED[file].default } }, {}) const routes = Object.keys(ROUTES).map((route) => { const path = route .replace(/\/src\/pages|index|\.tsx$/g, '') .replace(/\[\.{3}.+\]/, '*') .replace(/\[(.+)\]/, ':$1') return { path, component: lazy(ROUTES[route]) } }) export const Routes = () => { const App = preserved?.['_app'] || Fragment const NotFound = preserved?.['404'] || Fragment return ( <App> <Suspense fallback={'Loading...'}> <BrowserRoutes> {routes.map(({ path, component: Component = Fragment }) => ( <Route key={path} path={path} element={<Component />} /> ))} <Route path="*" element={<NotFound />} /> </BrowserRoutes> </Suspense> </App> ) }

What’s next

If you want to see a working example of this client-side file-based routing setup with code-splitting and pre-loading, checkout Render. It was quick and straight forward to upgrade to React Router v6.

I’ll soon cover combining the previous file-based routing setup with React Location and its interesting features for the client-side React applications.

I would love to hear what you think about this post, feel free to leave a comment on the discussion. If you have questions or got stuck at some point I’ll be happy to help.

Share this post if you find it useful and stay tuned for upcoming posts.