React Router Usage and Principle Analysis

We all know that React can develop an SPA, that is, Single Page Application. The so-called Single Page Application, as the name suggests, is that there is only one html doc on the web front end of the entire website. This kind of application is different from traditional websites. At the beginning of the development of the web, different pages correspond to different html, which means that you can see several pages with several different html. Every time the address changes, the address bar will re-issue a get request to the server, and then request back a different html doc.

But Single Page Application does not need, or even should not send a get request every time the route changes, because it only has one page, so in this case, how do we achieve only update the page when the route changes without initiating a new request?, which requires the use of react-router. Let’s take a look at the use of react-router and a simple introduction to the principle.

React

Using and Not Using React

Not using React.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React from 'react'
import { render } from 'react-dom'

const About = React.createClass({/*...*/})
const Inbox = React.createClass({/*...*/})
const Home = React.createClass({/*...*/})

const App = React.createClass({
getInitialState() {
return {
route: window.location.hash.substr(1)
}
},

componentDidMount() {
window.addEventListener('hashchange', () => {
this.setState({
route: window.location.hash.substr(1)
})
})
},

render() {
let Child
switch (this.state.route) {
case '/about': Child = About; break;
case '/inbox': Child = Inbox; break;
default: Child = Home;
}

return (
<div>
<h1>App</h1>
<ul>
<li><a href="#/about">About</a></li>
<li><a href="#/inbox">Inbox</a></li>
</ul>
<Child/>
</div>
)
}
})

React.render(<App />, document.body)

** It can be seen that even without using React-Router, React itself provides the ability to click a button to achieve a page “jump”. **

However, there are several problems with this:

  • When the hash part of the URL (referring to the part after the # ) changes, ‘< App >’ will render a different ‘< Child >’ based on this.state.route. It seems straightforward, but it quickly becomes complicated.

  • can only support hash routing, if we need to implement the route is’/about 'this is not possible, it will cause the browser to send a new request

Use React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React from 'react'
import { render } from 'react-dom'

//First we need to import some components...
import { Router, Route, Link } from 'react-router'

//Then we remove a bunch of code from the app and
//Add some < Link > elements...
const App = React.createClass({
render() {
return (
<div>
<h1>App</h1>
{/* Turn < a > into < Link > */}
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>

{/*
Then replace < Child > with this.props.children
The router will help us find the children
*/}
{this.props.children}
</div>
)
}
})

//Finally, we render the < Router > with some < Route >.
These are the things we want provided by the route.
React.render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox} />
</Route>
</Router>
), document.body)

React Router knows how to build nested UIs for us, so we don’t have to manually figure out which < Child > components to render. For example, for a complete /about path, React Router will build < App > < About/> .

Internally, the router will convert your tree-level nested < Route > format into a route configuration. But if you are not familiar with JSX, you can also use normal objects instead:

1
2
3
4
5
6
7
8
9
10
const routes = {
path: '/',
component: App,
childRoutes: [
{ path: 'about', component: About },
{ path: 'inbox', component: Inbox },
]
}

React.render(<Router routes={routes} />, document.body)

Routing configuration

A route configuration is a set of instructions that tell a router how to match a URL and how to execute code after matching it. Let’s explain how to write a route configuration with a simple example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import React from 'react'
import { Router, Route, Link } from 'react-router'

const App = React.createClass({
render() {
return (
<div>
<h1>App</h1>
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>
{this.props.children}
</div>
)
}
})

const About = React.createClass({
render() {
return <h3>About</h3>
}
})

const Inbox = React.createClass({
render() {
return (
<div>
<h2>Inbox</h2>
{this.props.children || "Welcome to your Inbox"}
</div>
)
}
})

const Message = React.createClass({
render() {
return <h3>Message {this.props.params.id}</h3>
}
})

React.render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
), document.body)

Through the above configuration, this application knows how to render the following four URLs:

URLComponents
/App
/aboutApp -> About
/inboxApp -> Inbox
/inbox/message/:idApp -> Inbox -> Message

Add homepage

Imagine that when the URL is/, we want to render a component in the App. However, at this time, this.props.children in the render of the App is still undefined. In this case, we can use IndexRoute to set a default page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { IndexRoute } from 'react-router'

const Dashboard = React.createClass({
render() {
return <div>Welcome to the app!</div>
}
})

React.render((
<Router>
<Route path="/" component={App}>
{/* Render Dashboard when url is/*/}
<IndexRoute component={Dashboard} />
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
), document.body)

Now, this.props.children in the render of the App will be the < Dashboard > element. This function is similar to Apache’s DirectoryIndex and nginx’s index directive. The above functions allow you to formulate an entry file similar to index.html when the requested URL matches a directory.

The current sitemap is:

URLComponents
/App -> Dashboard
/aboutApp -> About
/inboxApp -> Inbox
/inbox/message/:idApp -> Inbox -> Message

Will

It would be nice if we could remove /inbox from /inbox/messages/: id and also have Message nested in App - > Inbox to render. Absolute paths allow us to do this.

1
2
3
4
5
6
7
8
9
10
11
12
React.render((
<Router>
<Route path="/" component={App}>
<IndexRoute component={Dashboard} />
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
{/* Replace messages/: id */with /messages/: id}
<Route path="/messages/:id" component={Message} />
</Route>
</Route>
</Router>
), document.body)

The ability to use absolute paths in multi-layer nested routes gives us absolute control over the URL. We don’t need to add more layers to the URL, so we can use more concise URLs.

Our current URL correspondence is as follows:

URLComponents
/App -> Dashboard
/aboutApp -> About
/inboxApp -> Inbox
/message/:idApp -> Inbox -> Message

Replace jsx with the configured way

Because routes are generally used nested, it is very convenient to use JSX, which naturally has a concise nesting syntax, to describe their relationship. However, if you do not want to use JSX, you can also use the native route array object directly.

The routing configuration we discussed above can be written as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const routeConfig = [
{ path: '/',
component: App,
indexRoute: { component: Dashboard },
childRoutes: [
{ path: 'about', component: About },
{ path: 'inbox',
component: Inbox,
childRoutes: [
{ path: '/messages/:id', component: Message },
{ path: 'messages/:id',
onEnter: function (nextState, replaceState) {
replaceState(null, '/messages/' + nextState.params.id)
}
}
]
}
]
}
]

React.render(<Router routes={routeConfig} />, document.body)

Routing matching principle

A route has three properties that determine whether it “matches” a URL:

  1. Nested relationships
  2. Its path syntax
  3. Its priority

React Router uses the concept of route nesting to let you define a nested collection of views. ** When a given URL is called, the entire collection (the hit part) will be rendered. ** Nested routes are described as a tree structure. React Router will deep-first traverse the entire route configuration to find a route that matches the given URL.

Path syntax

A routing path is a string pattern that matches a URL (or a portion of it). Most routing paths can be understood literally, except for the following special symbols:

-: paramName - Matches a URL after/,? or # . The hit part will be taken as a parameter

  • () – inside it is considered optional
  • * – match arbitrary characters (non-greedy) until the next character or the end of the entire URL is hit, and create a splat parameter
1
2
3
<Route path="/hello/:name">         // 匹配 /hello/michael 和 /hello/ryan
<Route path="/hello(/:name)"> // 匹配 /hello, /hello/michael 和 /hello/ryan
<Route path="/files/*.*"> // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg

If a route uses relative paths, the complete path will be composed of the paths of all its ancestors and the relative paths specified by itself. Using absolute paths can make route matching behavior ignore nesting relationships.

Finally, the routing algorithm will match routes from top to bottom according to the defined order. Therefore, when you have two sibling Routing Node configurations, you must confirm that the previous route will not match the path in the latter route

Default route

IndexRoute

Before explaining the use case of the default route (IndexRoute), let’s imagine what a route configuration that does not use the default route looks like.

1
2
3
4
5
6
<Router>
<Route path="/" component={App}>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>

When the user accesses/, the App component is rendered, but the child elements within the component are not. The this.props.children inside the App is undefined. You can simply use '{this.props.children | |

} 'to render some default UI components.

But now, Home cannot participate in routing mechanisms such as onEnter hooks. In the Home position, Accounts and Statements are rendered. From this, the router allows you to use IndexRoute to make Home appear as the highest-level route.

1
2
3
4
5
6
7
<Router>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>

Now that the app can render {this.props.children}, we also have a top-level route that allows Home to participate.

Index

If you use < Link to = “/” > Home in this app, it will always be active because all URLs start with/. This is really a problem because we only want to activate and link to Home after it is rendered.

If a link to/needs to be activated after the Home route has been rendered, use < IndexLink to = “/” > Home

React

At the beginning, when we talked about the difference between using React Router and not using it, we said that in fact, there is no react-router, and you can also click on the connection to switch the page, but you can only use the hah method, so the react-router is actually trying to find a way to change the page component through the change of the pathname

This method is to use the’history API 'provided by the browser.

Write a simple demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function App() {
//When entering the page, first initialize the component name corresponding to the current url
let pathname = window.location.pathname
let initUI = pathname = '/login' ? 'login' : 'register'

let [UI, setUI] = useState (initUI);
let onClickLogin = () => {
setUI('Login')
window.history.pushState(null, '', '/login')
}
let onClickRegister = () => {
setUI('Register')
window.history.pushState(null, '', '/register')
}
let showUI = () => {
switch(UI) {
case 'Login':
return <Login/>
case 'Register':
return <Register/>
}
}
return (
<div className="App">
<button onClick={onClickLogin}>Login</button>
<button onClick={onClickRegister}>Register</button>
<div>
{showUI()}
</div>
</div>
);
}

History

React Router is built on top of history. In short, a history knows how to listen for changes in the browser address bar, parse this URL into a location object, then use it to match the router, and finally render the corresponding component correctly.

The commonly used history comes in three forms, but you can also implement a custom history using React Router.

  • browserHistory
  • hashHistory
  • createMemoryHistory

You can import them from React Router.

1
2
3
4
5
6
7
8
//JavaScript module import (Translator's Note: ES6 form)
import { browserHistory } from 'react-router'
Then pass them to < Router >.

render(
<Router history={browserHistory} routes={routes} />,
document.getElementById('app')
)

browserHistory

Browser history is the recommended history for applications using React Router. It uses the History API in the browser to process URLs, creating a real URL like example.com/some/path.

hash

If we can use the window.history API that comes with the browser, then our feature can be detected by the browser. If not, then any application that calls the jump will result in a full page refresh, which allows for a better User Experience when building applications and updating browsers, but still supports older versions.

You may wonder why we don’t go back to hash history, the problem is that these URLs are indeterminate. If a visitor shares a URL in hash history and browser history, and then they also share the same back function, we will eventually crash with an infinite number of URLs that produce a cartesian product.

Hash history uses the hash (#) part of the URL to create a route of example.com/#/some/path shape.

createMemoryHistory

Memory history will not be manipulated or read in the address bar. This explains how we implement server rendering. It is also very suitable for testing and other rendering environments (like React Native).

One difference from the other two histories is that you have to create it, which is easy to test.

const history = createMemoryHistory(location)