React Router V6: Changes and Reflections

R. Cory Stine
5 min readApr 15, 2022

About a week into developing my final project for Flatiron School — BoardGameShelf — I started stubbing out some routes using the node package React Router Dom. However, I quickly began running into console errors that I hadn’t anticipated. Based on my knowledge, the code was correctly laid out. But even after a review by some fellow students, I couldn’t uncover the source of the errors.

That is until I did some research into React Router Dom and discovered that I had started my project on the same week that a new version (V6) had been released. I was operating using outdated syntax. Once I reviewed the changes, I was able to update my code and get everything running the way I had envisioned.

React Router V6 is only a few months old at this point and I assume I will not be the only engineer to run into these errors that is more accustomed to an older version. As such, let’s take some time to dig into the new features, so that we can smoothly transition to these updates.

  1. Switch Is Out, Routes Is In — It’s All Relative

In previous versions of React Router, developers had the option to wrap their routes in a Switch component. Switch would review its child routes and render the first component that matched the URL that was input.

### Version 5import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route };
import Main from './components/Main.js';
import Login from './components/Login.js';
import SignUp from './components/SignUp.js';
class App extends Component {
render() {
return(
<Router>
<Switch>
<Route exact path='/' component={Main} />
<Route exact path='/signup' component={Signup} />
<Route exact path='/login' component={Login} />
</Switch>
</Router>
)
}
}
export default App;

However, in V6, the Switch component has been dropped for Routes. Routes is now a mandatory inclusion in your code and comes with some distinct benefits over the previous method. Any Route or Link component wrapped in Routes is relative, meaning that they are more efficient and easier to read. You also don’t need to be as concerned about the order of the routes, as the best route for the current URL entry will be automatically detected. We’ll see later how all of this makes utilizing nested routes much easier.

### Version 6 - Changes Noted in Boldimport React, { Component } from 'react';
import { BrowserRouter as Router, Routes, Route };
import Main from './components/Main.js';
import Login from './components/Login.js';
import SignUp from './components/SignUp.js';
class App extends Component {
render() {
return(
<Router>
<Routes>
<Route path='/' element={<Main />} />
<Route path='signup' element={<Signup />} />
<Route path='login' element={<Main />} />
</Routes>
</Router>
)
}
}
export default App;

Finally, relative routes allow us to omit the ‘/’ from our paths if we so choose. This is also the case in any Link components we might use later on.

<Route path='signup' element={<Signup />} />
<Link to='signup' />

2. Component Is Out, Element Is In

If you review the examples of V5 and V6 above, I’ve included another major syntactical change. Within our Route components, we no longer use the component prop. Instead, we use the element prop and pass in the JSX of the component we want to render when the associated URL path is called. In this case, if you were to navigate to ‘/signup’ the Signup component would render.

What’s awesome about this new technique is that because you can now pass JSX code into element, you can also send props down to the rendered component and include any JSX logic that may have previously been written outside the Route. It streamlines the whole process.

Here’s a simple example of how this could be implemented:

class App extends Component {   const games = ["Ticket to Ride", "Cosmic Encounter", "Game of                 
Thrones", "Shadows Over Camelot",
"Telestrations", "Pandemic"]
render() {
return(
<Router>
<Routes>
<Route path='/' element={<Main />} />
<Route path='signup' element={<Signup />} />
<Route path='login' element={<Main />} />
<Route
path='games'
element={<Games games={games} />}
/>
</Routes>
</Router>
)
}
}

3. Cut Exact

You might have also noticed that when transitioning from V5 to V6, we no longer have to include the exact keyword within our Route component. In previous versions, exact disabled partial matching of routes and would ensure that only the exact match of an entered route would be returned. In V6, that is the default functionality of a Route component, so the exact keyword is no longer necessary.

4. Simplifying Nested Routes

Maybe the most significant change to React Router DOM, and the place where V6 shines the most, is the handling of nested routes. In V5, nested routes were built within child components. Let’s take a look:

### Version 5//Contained in App.js
<Route exact path='/games' component={Games} />
//Contained in Games.jsimport { useRouteMatch } from 'react-router-dom';
import Game from './Game.js';
const Games = () => {
const { path } = useRouteMatch();
return (
<div className="all-games-containter">
<Switch>
<Route path={`${path}/:id`} component={Game} />
</Switch>
</div>
);
}

This is a fairly simplistic example, but you can see how things could get messy pretty quickly. Routes are going to spread throughout your entire application to any component that requires nesting. That’s a lot of extra code to keep track of and isn’t necessarily the most efficient option.

With V6, all of our nested routes can be easily contained within our original Routes wrapping — all within a single file. All we need to do is wrap our nested routes within the parent route.

class App extends Component {render() {
return(
<Router>
<Routes>
<Route path='/' element={<Main />} />
<Route path='signup' element={<Signup />} />
<Route path='login' element={<Main />} />
<Route path='games' element={<Games />}>
<Route path=':id' element={<GameDetails />} />
</Route>

</Routes>
</Router>
)
}
}

Then, in the parent component, we insert the new Outlet element, which will serve as a placeholder to determine where the child element renders.

//Contained in Games.jsimport { Outlet } from 'react-router-dom';const Games = () => {return (
<div className="all-games-containter">
<Outlet />
</div>
);
}

Now, when a user navigates to a specific game (i.e. /games/1), React Router will render the GameDetails component for that game. Outlet is simply acting as a guide.

This is much cleaner and allows us to contain all of our Routes at the highest level, rather then scattering them about the application.

5. useNavigate vs useHistory

Sometimes, you may want to navigate to a page when a user takes an action, such as when they submit a form or close out of a modal. Traditionally, to do program this type of action, we would use the useHistory() hook.

### Version 5import { useHistory } from 'react-router-dom';const GameDetails = () => {
const history = useHistory();
const handleCloseClick = () => {
history.push('/games')
}

return(
<div className="close">
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={handleCloseClick}
/>
</div>
)
}

In this example, I’ve created a function called handleCloseClick that will navigate the user back to ‘/games’ when they finish looking at the GameDetails component and click the close button.

The new method, useNavigate(), is slightly more intuitive and very simple to implement.

### Version 6import { useNavigate } from 'react-router-dom';const GameDetails = () => {
const navigate = useNavigate();
const handleCloseClick = () => {
navigate('/games')
}

return(
<div className="close">
<button
type="button"
className="btn-close"
aria-label="Close"
onClick={handleCloseClick}
/>
</div>
)
}

React Router V6 features a number of others changes, but there are the ones that are most likely to impact your workflow. While this update did initially disrupt my project work, it ended up being a blessing in disguise. Almost every new option makes the code cleaner, less repetitive and more readable. It’s a great addition to an already invaluable tool.

--

--