Chapter 9. Handling Navigation with Routes

Almost every web application requires routing. It is the process of responding to a given URL, based on a set of route configurations. A simple mapping, in other words, from the URL to rendered content. However, this simple task is more involved than it seems at first. This is why we're going to leverage the react-router package in this chapter, the de facto routing tool for React.

First, you'll learn the basics of declaring routes using JSX syntax. Then, we'll dig into the dynamic aspects of routing, such as dynamic path segments and query parameters. Next, you'll implement links using components from react-router. We'll close the chapter with an example that shows you how to lazy-load your components as the URL changes.

Declaring routes

If you've ever worked with routing outside of React, you probably already know that it can get messy quickly. With react-router, it's much easier to collocate routes with the content that they render. In this section, you'll see that this is due in large part to the declarative JSX syntax used to define routes.

We'll create a basic hello world example route so that you can get a basic handle on what routes look like in React applications. Then, we'll look at how you can organize your route declarations by feature instead of in a monolithic module. Finally, you'll implement a common parent-child routing pattern.

Hello route

Let's create a simple route that renders a simple component. First, we have the simplest possible React component that we want to render when the route is activated:

import React from 'react'; 
 
export default () => ( 
  <p>Hello Route!</p> 
); 

Next, let's look at the route definition itself:

import React from 'react'; 
import { 
  Router, 
  Route, 
  browserHistory, 
} from 'react-router'; 
 
import MyComponent from './MyComponent'; 
 
// Exports a "<Router>" component to be rendered. 
export default ( 
  <Router history={browserHistory}> 
    <Route path="/" component={MyComponent} /> 
  </Router> 
); 

As you can see, this module exports a JSX <Router> element. This is actually the top-level component of the application, as you'll see in a moment. But first, let's break down what's happening here. The browserHistory object is used to tell the Router that it should be using the native browser history API to keep track of navigation. Don't worry about this property too much; the majority of the time, you'll use this option.

Within the router, we have the actual routes declared as <Route> elements. There are two key properties of any route: path and component. These are pretty self-explanatory, when the path is matched against the active URL, the component is rendered. But where is it rendered, exactly?

To help answer this, let's take a look at the main module of the application:

import React from 'react'; 
import { render } from 'react-dom'; 
 
// Imports the "<Router>", and all the "<Route>" 
// elements within. 
import routes from './routes'; 
 
// The "<Router>" is the root element of the app. 
render( 
  routes, 
  document.getElementById('app') 
); 

As I mentioned earlier, the router is the top-level component. The router doesn't actually render anything itself, it's merely responsible for managing how other components are rendered based on the current URL. Sure enough, when you look at this example in a browser, <MyComponent> is rendered as expected:

Hello route

That's the gist of how routing works in React. Let's look at declaring routes from the perspective of features now.

Decoupling route declarations

As the preceding section illustrated, declaring routes is nice and easy with react-router. The difficulty comes when your application grows and dozens of routes are all declared within a single module, because it's more difficult to mentally map a route to a feature.

To help with this, each top-level feature of the application can define its own routes module. This way, it's clear which routes belong to which feature. So, let's start with the root router module and see how this looks when it pulls in feature-specific routes:

import React from 'react'; 
import { 
  Router, 
  browserHistory, 
} from 'react-router'; 
 
// Import the routes from our features. 
import oneRoutes from './one/routes'; 
import twoRoutes from './two/routes'; 
 
// The feature routes are rendered as children of 
// the main router. 
export default ( 
  <Router history={browserHistory}> 
    {oneRoutes} 
    {twoRoutes} 
  </Router> 
); 

In this example, the application has two phenomenally named features: one and two. The oneRoutes and twoRoutes values are pulled in from their respective feature directories and rendered as children in the <Router> element. Now this module will only get as big as the number of application features, instead of the number of routes, which could be substantially larger. Let's take a look at one of the feature routers now:

import React from 'react'; 
import { Route, IndexRedirect } from 'react-router'; 
 
// The pages that make up feature "one". 
import First from './First'; 
import Second from './Second'; 
 
// The routes of our feature. The "<IndexRedirect>" 
// handles "/one" requests by redirecting to "/one/1". 
export default ( 
  <Route path="/one"> 
    <IndexRedirect to="1" /> 
    <Route path="1" component={First} /> 
    <Route path="2" component={Second} /> 
  </Route> 
); 

This module exports a single <Route> element. Every route that this feature uses is a child <Route>. For example, if you run this example and point your browser to /one/1, you'll see the rendered content of First.

Decoupling route declarations

The <IndexRedirect> element is necessary to redirect /one to /one/1.

Toward the end of the chapter, we'll elaborate some more on how to structure routes and application features. But first, we need to think about parent and child routes in more depth.

Parent and child routes

The App component in the preceding example was the main component of the application. This is because it defined the root URL: /. However, once the user navigated to a specific feature URL, the App component was no longer relevant.

The challenge with routing is that sometimes you need a parent component, such as App, so that you don't have to keep re-rendering the page skeleton. By skeleton, I mean the content that's common to every page of the application. In this scenario, we always want App to render. When the URL changes, only certain parts of the App component render new content. Here's a diagram that illustrates the idea:

Parent and child routes

As the URL changes, the parent component is populated with the appropriate child components. Starting with the App component, let's figure out how to make this setup work:

import React, { PropTypes } from 'react'; 
 
// The "header" and "main" properties are the rendered 
// components specified in the route. They're placed 
// in the JSX of this component - "App". 
const App = ({ header, main }) => ( 
  <section> 
    <header> 
      {header} 
    </header> 
    <main> 
      {main} 
    </main> 
  </section> 
); 
 
// The "header" and "main" properties should be 
// a React element. 
App.propTypes = { 
  header: PropTypes.element, 
  main: PropTypes.element, 
}; 

Now we have the page skeleton defined. It's now up to the router configuration to correctly render App. Let's look at this configuration now:

import React from 'react'; 
import { 
  Router, 
  Route, 
  browserHistory, 
} from 'react-router'; 
 
// The "App" component is rendered with every route. 
import App from './App'; 
 
// The "User" components rendered with the "/users" 
// route. 
import UsersHeader from './users/UsersHeader'; 
import UsersMain from './users/UsersMain'; 
 
// The "Groups" components rendered with the "/groups" 
// route. 
import GroupsHeader from './groups/GroupsHeader'; 
import GroupsMain from './groups/GroupsMain'; 
 
// Configures the "/users" route. It has a path, 
// and named components that are placed in "App". 
const users = { 
  path: 'users', 
  components: { 
    header: UsersHeader, 
    main: UsersMain, 
  }, 
}; 
 
// Configures the "/groups" route. It has a path, 
// and named components that are placed in "App". 
const groups = { 
  path: 'groups', 
  components: { 
    header: GroupsHeader, 
    main: GroupsMain, 
  }, 
}; 
 
// Setup the router, using the "users" and "groups" 
// route configurations. 
export default ( 
  <Router history={browserHistory}> 
    <Route path="/" component={App}> 
      <Route {...users} /> 
      <Route {...groups} /> 
    </Route> 
  </Router> 
); 

This application has two pages/features—users and groups. Each of them has its own App components defined. For example, UsersHeader fills in the header value and UsersMain fills in the main value. Also note that each of the routes has a components property, where you can pass both the header and main values. This is how App is able to find the content that it needs based on the current route. If you run this example and navigate to /users, here's what you can expect to see:

Parent and child routes

A common mistake for newcomers to react-router is to think that the components themselves are passed to the parent. In reality, it's the rendered JSX element that's passed to the parent. The router actually renders the content and then passes the content to the parent.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset