{"id":257428,"date":"2017-08-07T04:53:19","date_gmt":"2017-08-07T11:53:19","guid":{"rendered":"http:\/\/css-tricks.com\/?p=257428"},"modified":"2019-03-28T08:47:57","modified_gmt":"2019-03-28T15:47:57","slug":"react-router-4","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/react-router-4\/","title":{"rendered":"All About React Router 4"},"content":{"rendered":"
I met Michael Jackson<\/a> for the first time at React Rally 2016, soon after writing an article on React Router 3<\/a>. Michael is one of the principal authors of React Router along with Ryan Florence<\/a>. It was exciting to meet someone who built a tool I liked so much, but I was shocked when he said. “Let me show you our ideas React Router 4, it’s way<\/em> different!” Truthfully, I didn’t understand the new direction and why it needed such big changes. Since the router is such a big part of an application’s architecture, this would potentially change some patterns I’ve grown to love. The idea of these changes gave me anxiety. Considering community cohesiveness and being that React Router plays a huge role in so many React applications, I didn’t know how the community would accept the changes.<\/p>\n A few months later, React Router 4<\/a> was released, and I could tell just from the Twitter buzz there was mixed feelings on the drastic re-write. It reminded me of the push-back the first version of React Router had for its progressive concepts. In some ways, earlier versions of React Router resembled our traditional mental model of what an application router “should be” by placing all the routes rules in one place. However, the use of nested JSX routes wasn’t accepted by everyone. But just as JSX itself overcame its critics (at least most of them), many came around to believe that a nested JSX router was a pretty cool idea. <\/p>\n So, I learned React Router 4. Admittedly, it was a struggle the first day. The struggle was not with the API, but more so the patterns and strategy for using it. My mental model for using React Router 3 wasn’t migrating well to v4. I would have to change how I thought about the relationship between the router and the layout components if I was going to be successful. Eventually, new patterns emerged that made sense to me and I became very happy with the router’s new direction. React Router 4 allowed me to do everything I could do with v3, and more. Also, at first, I was over-complicating the use of v4. Once I gained a new mental model for it, I realized that this new direction is amazing!<\/p>\n My intentions for this article aren’t to rehash the already well-written documentation<\/a> for React Router 4. I will cover the most common API concepts, but the real focus is on patterns and strategies that I’ve found to be successful. <\/p>\n React Router 5 is now available which is backwards compatible with React Router 4. It mostly has bug fixes and internal enhancements<\/a> to make it more compatible with React 16.<\/p>\n Here are some JavaScript concepts you need to be familiar with for this article:<\/p>\n If you’re the type that prefers jumping right to a working demo, here you go:<\/p>\n View Demo<\/a><\/p>\n Earlier versions of React Router centralized the routing rules into one place, keeping them separate from layout components. Sure, the router could be partitioned and organized into several files, but conceptually the router was a unit, and basically a glorified configuration file. <\/p>\n Perhaps the best way to see how v4 is different is to write a simple two-page app in each version and compare. The example app has just two routes for a home page and a user’s page.<\/p>\n Here it is in v3: <\/p>\n Here are some key concepts in v3 that are not true in v4 anymore:<\/p>\n React Router 4 does not advocate for a centralized router anymore. Instead, routing rules live within the layout and amongst the UI itself. As an example, here’s the same application in v4:<\/p>\n New API Concept<\/strong>: Since our app is meant for the browser, we need to wrap it in The first thing that stands out when looking at an app built with React Router v4 is that the “router” seems to be missing. In v3 the router was this giant thing we rendered directly to the DOM which orchestrated our application. Now, besides Another v3-staple missing from the v4 example is the use of In the previous example, you may have noticed the In the previous example, we’re trying to render either the To understand the matching logic better, review path-to-regexp<\/a> which is what v4 now uses to determine whether routes match the URL.<\/p>\n To demonstrate how inclusive routing is helpful, let’s include a Now, when the user visits `\/users`, both components will render. Something like this was doable in v3 with certain patterns, but it was more difficult. Thanks to v4’s inclusive routes, it’s now a breeze. <\/p>\n If you need just one route to match in a group, use Only one of the routes in a given Sure, we could put them in any order if we use The While there is no more You’re probably starting to anticipate nested sub layouts and how you might achieve them. I didn’t think I would struggle with this concept, but I did. React Router v4 gives us a lot of options, which makes it powerful. Options, though, means the freedom to choose strategies that are not ideal. On the surface, nested layouts are trivial, but depending on your choices you may experience friction because of the way you organized the router. <\/p>\n To demonstrate, let’s imagine that we want to expand our users section so we have a “browse users” page and a “user profile” page. We also want similar pages for products. Users and products both need sub-layout that are special and unique to each respective section. For example, each might have different navigation tabs. There are a few approaches to solve this, some good and some bad. The first approach is not very good but I want to show you so you don’t fall into this trap. The second approach is much better.<\/p>\n For the first, let’s modify our While this does technically work, taking a closer look at the two user pages starts to reveal the problem:<\/p>\n New API Concept:<\/strong> Each user page not only renders its respective content but also has to be concerned with the sub layout itself (and the sub layout is repeated for each). While this example is small and might seem trivial, repeated code can be a problem in a real application. Not to mention, each time a Here’s a different approach which is better:<\/p>\n Instead of four routes corresponding to each of the user’s and product’s pages, we have two routes for each section’s layout instead.<\/p>\n Notice the above routes do not use the With this strategy, it becomes the task of the sub layouts to render additional routes. Here’s what the The most obvious win in the new strategy is that the layout isn’t repeated among all the user pages. It’s a double win too because it won’t have the same lifecycle problems as with the first example.<\/p>\n One thing to notice is that even though we’re deeply nested in our layout structure, the routes still need to identify their full path in order to match. To save yourself the repetitive typing (and in case you decide to change the word “users” to something else), use As we’ve seen so far, The differences between these two can seem unclear at first. Console logging them can sometimes reveal the same output making their differences even more unclear. For example, both these console logs will output the same value when the browser path is `\/users`:<\/p>\n ES2015 Concept:<\/strong> While we can’t see the difference yet, If you’re going to use one of these to help build your route paths, I urge you to choose To illustrate the problem, I’m rendering two sub components with one route path being made from So why does It wasn’t until later that I saw this part of the documentation<\/a> and realized how important it was:<\/p>\n \nmatch: <\/p>\n Let’s assume the app we’re making is a dashboard so we want to be able to add and edit users by visiting `\/users\/add` and `\/users\/5\/edit`. But with the previous examples, Notice that the add and edit routes strategically come before the profile route to ensure there the proper matching. Had the profile path been first, visiting `\/users\/add` would have matched the profile (because “add” would have matched the Alternatively, we can put the profile route first if we make the path It’s very common in applications to restrict the user’s ability to visit certain routes depending on their login status. Also common is to have a “look-and-feel” for the unauthorized pages (like “log in” and “forgot password”) vs the “look-and-feel” for the authorized ones (the main part of the application). To solve each of these needs, consider this main entry point to an application:<\/p>\n Using react-redux<\/a> works very similarly with React Router v4 as it did before, simply wrap There are a few takeaways with this approach. The first being that I’m choosing between two top-level layouts depending on which section of the application we’re in. Visiting paths like `\/auth\/login` or `\/auth\/forgot-password` will utilize the While your login strategy might differ from mine, I use a network request to Click here to see a fully working Authentication Example at CodePen<\/a>.<\/p>\n\n
A New API and A New Mental Model<\/h3>\n
import { Router, Route, IndexRoute } from 'react-router'\r\n\r\nconst PrimaryLayout = props => (\r\n <div className=\"primary-layout\">\r\n <header>\r\n Our React Router 3 App\r\n <\/header>\r\n <main>\r\n {props.children}\r\n <\/main>\r\n <\/div>\r\n)\r\n\r\nconst HomePage =() => <div>Home Page<\/div>\r\nconst UsersPage = () => <div>Users Page<\/div>\r\n\r\nconst App = () => (\r\n <Router history={browserHistory}>\r\n <Route path=\"\/\" component={PrimaryLayout}>\r\n <IndexRoute component={HomePage} \/>\r\n <Route path=\"\/users\" component={UsersPage} \/>\r\n <\/Route>\r\n <\/Router>\r\n)\r\n\r\nrender(<App \/>, document.getElementById('root'))<\/code><\/pre>\n
\n
<Route><\/code> components.<\/li>\n
import { BrowserRouter, Route } from 'react-router-dom'\r\n\r\nconst PrimaryLayout = () => (\r\n <div className=\"primary-layout\">\r\n <header>\r\n Our React Router 4 App\r\n <\/header>\r\n <main>\r\n <Route path=\"\/\" exact component={HomePage} \/>\r\n <Route path=\"\/users\" component={UsersPage} \/>\r\n <\/main>\r\n <\/div>\r\n)\r\n\r\nconst HomePage =() => <div>Home Page<\/div>\r\nconst UsersPage = () => <div>Users Page<\/div>\r\n\r\nconst App = () => (\r\n <BrowserRouter>\r\n <PrimaryLayout \/>\r\n <\/BrowserRouter>\r\n)\r\n\r\nrender(<App \/>, document.getElementById('root'))<\/code><\/pre>\n
<BrowserRouter><\/code> which comes from v4. Also notice we import from
react-router-dom<\/code> now (which means we
npm install react-router-dom<\/code> not
react-router<\/code>). Hint! It’s called
react-router-dom<\/code> now because there’s also a native version<\/a>.<\/p>\n
<BrowserRouter><\/code>, the first thing we throw into the DOM is our application itself.<\/p>\n
{props.children}<\/code> to nest components. This is because in v4, wherever the
<Route><\/code> component is written is where the sub-component will render to if the route matches.<\/p>\n
Inclusive Routing<\/h3>\n
exact<\/code> prop. So what’s that all about? V3 routing rules were “exclusive” which meant that only one route would win. V4 routes are “inclusive” by default which means more than one
<Route><\/code> can match and render at the same time. <\/p>\n
HomePage<\/code> or the
UsersPage<\/code> depending on the path. If the
exact<\/code> prop were removed from the example, both the
HomePage<\/code> and
UsersPage<\/code> components would have rendered at the same time when visiting `\/users` in the browser.<\/p>\n
UserMenu<\/code> in the header, but only if we’re in the user’s part of our application:<\/p>\n
const PrimaryLayout = () => (\r\n <div className=\"primary-layout\">\r\n <header>\r\n Our React Router 4 App\r\n <Route path=\"\/users\" component={UsersMenu} \/>\r\n <\/header>\r\n <main>\r\n <Route path=\"\/\" exact component={HomePage} \/>\r\n <Route path=\"\/users\" component={UsersPage} \/>\r\n <\/main>\r\n <\/div>\r\n)<\/code><\/pre>\n
Exclusive Routing<\/h3>\n
<Switch><\/code> to enable exclusive routing:<\/p>\n
const PrimaryLayout = () => (\r\n <div className=\"primary-layout\">\r\n <PrimaryHeader \/>\r\n <main>\r\n <Switch>\r\n <Route path=\"\/\" exact component={HomePage} \/>\r\n <Route path=\"\/users\/add\" component={UserAddPage} \/>\r\n <Route path=\"\/users\" component={UsersPage} \/>\r\n <Redirect to=\"\/\" \/>\r\n <\/Switch>\r\n <\/main>\r\n <\/div>\r\n)<\/code><\/pre>\n
<Switch><\/code> will render. We still need
exact<\/code> on the
HomePage<\/code> route though if we’re going to list it first. Otherwise the home page route would match when visiting paths like `\/users` or `\/users\/add`. In fact, strategic placement is the name-of-the-game when using an exclusive routing strategy (as it always has been with traditional routers). Notice that we strategically place the routes for
\/users\/add<\/code> before
\/users<\/code> to ensure the correct matching. Since the path
\/users\/add<\/code> would match for `\/users` and `\/users\/add`, putting the
\/users\/add<\/code> first is best.<\/p>\n
exact<\/code> in certain ways, but at least we have options.<\/p>\n
<Redirect><\/code> component will always do a browser-redirect if encountered, but when it’s in a
<Switch><\/code> statement, the redirect component only gets rendered if no other routes match first. To see how
<Redirect><\/code> might be used in a non-switch circumstance, see Authorized Route<\/strong> below.<\/p>\n
“Index Routes” and “Not Found”<\/h3>\n
<IndexRoute><\/code> in v4, using
<Route exact><\/code> achieves the same thing. Or if no routes resolved, then use
<Switch><\/code> with
<Redirect><\/code> to redirect to a default page with a valid path (as I did with
HomePage<\/code> in the example), or even a not-found page.<\/p>\n
Nested Layouts<\/h3>\n
PrimaryLayout<\/code> to accommodate the browsing and profile pages for users and products:<\/p>\n
const PrimaryLayout = props => {\r\n return (\r\n <div className=\"primary-layout\">\r\n <PrimaryHeader \/>\r\n <main>\r\n <Switch>\r\n <Route path=\"\/\" exact component={HomePage} \/>\r\n <Route path=\"\/users\" exact component={BrowseUsersPage} \/>\r\n <Route path=\"\/users\/:userId\" component={UserProfilePage} \/>\r\n <Route path=\"\/products\" exact component={BrowseProductsPage} \/>\r\n <Route path=\"\/products\/:productId\" component={ProductProfilePage} \/>\r\n <Redirect to=\"\/\" \/>\r\n <\/Switch>\r\n <\/main>\r\n <\/div>\r\n )\r\n}<\/code><\/pre>\n
const BrowseUsersPage = () => (\r\n <div className=\"user-sub-layout\">\r\n <aside>\r\n <UserNav \/>\r\n <\/aside>\r\n <div className=\"primary-content\">\r\n <BrowseUserTable \/>\r\n <\/div>\r\n <\/div>\r\n)\r\n\r\nconst UserProfilePage = props => (\r\n <div className=\"user-sub-layout\">\r\n <aside>\r\n <UserNav \/>\r\n <\/aside>\r\n <div className=\"primary-content\">\r\n <UserProfile userId={props.match.params.userId} \/>\r\n <\/div>\r\n <\/div>\r\n)<\/code><\/pre>\n
props.match<\/code> is given to any component rendered by
<Route><\/code>. As you can see, the
userId<\/code> is provided by
props.match.params<\/code>. See more in v4 documentation<\/a>. Alternatively, if any component needs access to
props.match<\/code> but the component wasn’t rendered by a
<Route><\/code> directly, we can use the withRouter()<\/a> Higher Order Component.<\/p>\n
BrowseUsersPage<\/code> or
UserProfilePage<\/code> is rendered, it will create a new instance of
UserNav<\/code> which means all of its lifecycle methods start over. Had the navigation tabs required initial network traffic, this would cause unnecessary requests \u2014 all because of how we decided to use the router. <\/p>\n
const PrimaryLayout = props => {\r\n return (\r\n <div className=\"primary-layout\">\r\n <PrimaryHeader \/>\r\n <main>\r\n <Switch>\r\n <Route path=\"\/\" exact component={HomePage} \/>\r\n <Route path=\"\/users\" component={UserSubLayout} \/>\r\n <Route path=\"\/products\" component={ProductSubLayout} \/>\r\n <Redirect to=\"\/\" \/>\r\n <\/Switch>\r\n <\/main>\r\n <\/div>\r\n )\r\n}<\/code><\/pre>\n
exact<\/code> prop anymore because we want
\/users<\/code> to match any route that starts with
\/users<\/code> and similarly for products.<\/p>\n
UserSubLayout<\/code> could look like:<\/p>\n
const UserSubLayout = () => (\r\n <div className=\"user-sub-layout\">\r\n <aside>\r\n <UserNav \/>\r\n <\/aside>\r\n <div className=\"primary-content\">\r\n <Switch>\r\n <Route path=\"\/users\" exact component={BrowseUsersPage} \/>\r\n <Route path=\"\/users\/:userId\" component={UserProfilePage} \/>\r\n <\/Switch>\r\n <\/div>\r\n <\/div>\r\n)<\/code><\/pre>\n
props.match.path<\/code> instead:<\/p>\n
const UserSubLayout = props => (\r\n <div className=\"user-sub-layout\">\r\n <aside>\r\n <UserNav \/>\r\n <\/aside>\r\n <div className=\"primary-content\">\r\n <Switch>\r\n <Route path={props.match.path} exact component={BrowseUsersPage} \/>\r\n <Route path={`${props.match.path}\/:userId`} component={UserProfilePage} \/>\r\n <\/Switch>\r\n <\/div>\r\n <\/div>\r\n)<\/code><\/pre>\n
Match<\/h3>\n
props.match<\/code> is useful for knowing what
userId<\/code> the profile is rendering and also for writing our routes. The
match<\/code> object gives us several properties including
match.params<\/code>,
match.path<\/code>,
match.url<\/code> and several more<\/a>. <\/p>\n
match.path<\/strong> vs match.url<\/strong><\/h4>\n
const UserSubLayout = ({ match }) => {\r\n console.log(match.url) \/\/ output: \"\/users\"\r\n console.log(match.path) \/\/ output: \"\/users\"\r\n return (\r\n <div className=\"user-sub-layout\">\r\n <aside>\r\n <UserNav \/>\r\n <\/aside>\r\n <div className=\"primary-content\">\r\n <Switch>\r\n <Route path={match.path} exact component={BrowseUsersPage} \/>\r\n <Route path={`${match.path}\/:userId`} component={UserProfilePage} \/>\r\n <\/Switch>\r\n <\/div>\r\n <\/div>\r\n )\r\n}<\/code><\/pre>\n
match<\/code> is being destructured<\/a> at the parameter level of the component function. This means we can type
match.path<\/code> instead of
props.match.path<\/code>.<\/p>\n
match.url<\/code> is the actual path in the browser URL and
match.path<\/code> is the path written for the router. This is why they are the same, at least so far. However, if we did the same console logs one level deeper in
UserProfilePage<\/code> and visit `\/users\/5` in the browser,
match.url<\/code> would be
\"\/users\/5\"<\/code> and
match.path<\/code> would be
\"\/users\/:userId\"<\/code>.<\/p>\n
Which to choose?<\/h3>\n
match.path<\/code>. Using
match.url<\/code> to build route paths will eventually lead a scenario that you don’t want. Here’s a scenario which happened to me. Inside a component like
UserProfilePage<\/code> (which is rendered when the user visits `\/users\/5`), I rendered sub components like these:<\/p>\n
const UserComments = ({ match }) => (\r\n <div>UserId: {match.params.userId}<\/div>\r\n)\r\n\r\nconst UserSettings = ({ match }) => (\r\n <div>UserId: {match.params.userId}<\/div>\r\n)\r\n\r\nconst UserProfilePage = ({ match }) => (\r\n <div>\r\n User Profile:\r\n <Route path={`${match.url}\/comments`} component={UserComments} \/>\r\n <Route path={`${match.path}\/settings`} component={UserSettings} \/>\r\n <\/div>\r\n)<\/code><\/pre>\n
match.url<\/code> and one from
match.path<\/code>. Here’s what happens when visiting these pages in the browser: <\/p>\n
\n
match.path<\/code> work for helping to build our paths and
match.url<\/code> doesn’t? The answer lies in the fact that
{${match.url}\/comments}<\/code> is basically the same thing as if I had hard-coded
{'\/users\/5\/comments'}<\/code>. Doing this means the subsequent component won’t be able to fill
match.params<\/code> correctly because there were no params in the path, only a hardcoded
5<\/code>.<\/p>\n
\n
<Route><\/code>s<\/strong><\/li>\n
<Link><\/code>s<\/strong><\/li>\n<\/ul>\n<\/blockquote>\n
Avoiding Match Collisions<\/h3>\n
users\/:userId<\/code> already points to a
UserProfilePage<\/code>. So does that mean that the route with
users\/:userId<\/code> now needs to point to yet another sub-sub-layout to accomodate editing and the profile? I don’t think so. Since both the edit and profile pages share the same user-sub-layout, this strategy works out fine:<\/p>\n
const UserSubLayout = ({ match }) => (\r\n <div className=\"user-sub-layout\">\r\n <aside>\r\n <UserNav \/>\r\n <\/aside>\r\n <div className=\"primary-content\">\r\n <Switch>\r\n <Route exact path={props.match.path} component={BrowseUsersPage} \/>\r\n <Route path={`${match.path}\/add`} component={AddUserPage} \/>\r\n <Route path={`${match.path}\/:userId\/edit`} component={EditUserPage} \/>\r\n <Route path={`${match.path}\/:userId`} component={UserProfilePage} \/>\r\n <\/Switch>\r\n <\/div>\r\n <\/div>\r\n)<\/code><\/pre>\n
:userId<\/code>.<\/p>\n
${match.path}\/:userId(\\\\d+)<\/code> which ensures that
:userId<\/code> must be a number. Then visiting `\/users\/add` wouldn’t create a conflict. I learned this trick in the docs for path-to-regexp<\/a>.<\/p>\n<\/div>\n
Authorized Route<\/h3>\n
class App extends React.Component {\r\n render() {\r\n return (\r\n <Provider store={store}>\r\n <BrowserRouter>\r\n <Switch>\r\n <Route path=\"\/auth\" component={UnauthorizedLayout} \/>\r\n <AuthorizedRoute path=\"\/app\" component={PrimaryLayout} \/>\r\n <\/Switch>\r\n <\/BrowserRouter>\r\n <\/Provider>\r\n )\r\n }\r\n}<\/code><\/pre>\n
<BrowserRouter><\/code> in
<Provider><\/code> and it’s all set.<\/p>\n
UnauthorizedLayout<\/code> \u2014 one that looks appropriate for those contexts. When the user is logged in, we’ll ensure all paths have an `\/app` prefix which uses
AuthorizedRoute<\/code> to determine if the user is logged in or not. If the user tries to visit a page starting with `\/app` and they aren’t logged in, they will be redirected to the login page.<\/p>\n
AuthorizedRoute<\/code> isn’t a part of v4 though. I made it myself with the help of v4 docs<\/a>. One amazing new feature in v4 is the ability to create your own routes for specialized purposes. Instead of passing a
component<\/code> prop into
<Route><\/code>, pass a
render<\/code> callback instead:<\/p>\n
class AuthorizedRoute extends React.Component {\r\n componentWillMount() {\r\n getLoggedUser()\r\n }\r\n\r\n render() {\r\n const { component: Component, pending, logged, ...rest } = this.props\r\n return (\r\n <Route {...rest} render={props => {\r\n if (pending) return <div>Loading...<\/div>\r\n return logged\r\n ? <Component {...this.props} \/>\r\n : <Redirect to=\"\/auth\/login\" \/>\r\n }} \/>\r\n )\r\n }\r\n}\r\n\r\nconst stateToProps = ({ loggedUserState }) => ({\r\n pending: loggedUserState.pending,\r\n logged: loggedUserState.logged\r\n})\r\n\r\nexport default connect(stateToProps)(AuthorizedRoute)<\/code><\/pre>\n
getLoggedUser()<\/code> and plug
pending<\/code> and
logged<\/code> into Redux state.
pending<\/code> just means the request is still in route.<\/p>\n