Drill down
Recently, I started drilling down to the bottom of those tools and frameworks' source code in order to enlighten me on software/system design (as a pretentious developer).
I think you've seen that there are many libraries which kernel are with only a few lines of code on Github (e.g. Reselect), and they are starred 1000+. (Actually, react-router isn't one from them though) Thus, I started to rethink myself: Isn't it too simple? I could even make one myself (not being self-delusional but arrogant :D). But after reviewing their source code, the fact proves me wrong: I'm young and foolish. Simple things are difficult to make.
After drilling down the react-router repository, I'd like to share some of the key points of how they made the router possible. So in this article, I'll demonstrate creating a sweet homemade react-router from scratch.
Basic Concepts
A very rudimentary router behavior would be:
- When a user clicks on the
<a>
tag, the browser will redirect to the target page without reload
In terms with the requirement above, let's imagine one simple solution to react to the URL change.
- Use history API to handle page redirects programmatically
- Use a "link" component (
<a>
tag) to intercept redirection click - Use a wrapper component to conditionally render contents by judging the URL
(Can't be any simpler)
In react-router, there are 5 basic APIs:
- BrowserRouter: exposes history API to its children components
- Router: child component of BrowserRouter (and other types of router) which will provide
- Link: redirects without reload
- Route: decides wether to render
- RouterContext: provides URL related context including history API
With these components above, I will make a mini project: hello world.
// It will jump between /hello and /world
<BrowserRouter>
<Link to="/hello">hello</Link>
<Link to="/world">world</Link>
<Route path="/hello" render={() => <div>hello</div>} />
<Route path="/world" render={() => <div>world</div>} />
</BrowserRouter>
Example repository
I created a repo with create-react-app.
Repo url: https://github.com/daiyanze/react-router-tear-down
The folder structure looks like this
- public
- src
- router/ # sweet homemade router
...
- App.js
- index.js
- .gitignore
- LICENSE
- package.json
- README.md
1.Clone
$ git clone https://github.com/daiyanze/react-router-tear-down
$ cd react-router-tear-down
2. Start the server
# start the server
$ yarn & yarn start
# use npm if you prefer
$ npm i & npm run start
🎉 My React is alive!
3. Simplify
I think I don't quite need styling sheets and service worker. So I'll remove these fancy stuff.
rm -f src/App.css
rm -f src/index.css
rm -f src/logo.svg
rm -f src/serviceWorker.js
And stuff in my little "hello world".
./src/App.js
import React from 'react'
function App() {
return (
<div></div>
);
}
export default App
./src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Now the page is completely blank. Let me create each of the components to bring it back to life.
Start scratching
Now I will start implementing these files under ./src/router
folder.
src/router/
RouterContext.js
Route.js
Router.js
BrowserRouter.js
Link.js
RouterContext
In order to let all of the components access some properties, I need to create a context to provide the URL related objects for the decendant components.
./src/router/RouterContext.js
import React from "react"
export const RouterContext = React.createContext()
As simple as that!
Route
The Route will render the content once the path
matches the current URL.
Properties:
- path : The matching URL
-
children : The children component that ignores the
path
matching -
render : Similar to children but considers
path
matching -
component : Invokes
React.cloneElement
For the component properties, there's a rendering order.
children > component > render
This means when all of the above props come up in the same component, only children
will render. If children
doesn't exist, then component
will take effect. Else then render
...
render () {
return (
<Route
children={ChildrenComponents} // This will be rendered
component={Component} // Nope
render={() => Component} // Nope
/>
)
}
./src/router/Route.js
import React, { Component } from "react"
import { RouterContext } from "./RouterContext"
import { matchPath } from "react-router" // allow me to borrow its beautiful function
export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
const { location } = context
const { path, children, component, render, computedMatch } = this.props
const match = computedMatch
? computedMatch
: path ? matchPath(location.pathname, this.props) : context.match
const props = {
...context,
location,
match
}
// If match
// then children > component > render
// else
// children | null
return (
// Need to use context again to ensure the provider passes the updated props
<RouterContext.Provider value={props}>
{
match
? children // Children comes first
? (typeof children === "function" ? children(props) : children)
: (component // Component comes second
? React.createElement(component, props)
: (render ? render(props) : null)) // Render comes last
: (typeof children === "function" ? children(props) : null)
}
</RouterContext.Provider>
)
}
}
</RouterContext.Consumer>
)
}
}
BrowserRouter
The BrowserRouter uses HTML5 history API (pushState
replaceState
popState
etc.) to sync up the UI and URL.
All of those components that needs the history context have to stay as BrowserRouter children component. Otherwise, it will throw errors.
Properties:
- basename : The base of your app's entire URL
<!-- For instance, if "/app" is our URL base, "/app" will be prepended to all URLs -->
<BrowserRouter basename="/app">
<Link to="/somewhere">
Take me to somewhere
</Link>
</BrowserRouter>
<!-- The above will be compiled to -->
<a href="/app/somewhere">Take me to somewhere</a>
./src/router/BrowserRouter.js
import React, { Component } from "react"
import { createBrowserHistory } from "history"
import Router from "./Router"
export default class BrowserRouter extends Component {
constructor(props) {
super(props)
this.history = createBrowserHistory()
}
render() {
return <Router children={this.props.children} history={this.history} />
}
}
Router
In react-router, Router component is just a context provider which will listen to location
changes. It absorbs the history passed from BrowserRouter as its state.
./src/router/Router.js
import React, { Component } from "react"
import { RouterContext } from "./RouterContext"
export default class Router extends Component {
// Check root path
static computeRootMatch (pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
}
}
constructor (props) {
super(props)
this.state = {
location: props.history.location
}
}
componentDidMount () {
// update location when url changes
this.props.history.listen(({ location }) => {
this.setState({ location })
})
}
render () {
const { history, children } = this.props
return (
// Provide `history` `location` `match` to children components
<RouterContext.Provider
value={{
history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname)
}}>
{ children }
</RouterContext.Provider>
)
}
}
Link
All I need to do is to intercept the click event and use the history API to change the url.
./src/router/Link.js
import React, { Component } from "react"
import { RouterContext } from "./RouterContext"
export default class Link extends Component {
static contextType = RouterContext
handleClick = e => {
e.preventDefault()
// Push the target location to history
this.context.history.push(this.props.to)
}
render() {
const { to, children, ...others } = this.props
return (
<a onClick={this.handleClick} href={to} {...others}>
{ children }
</a>
)
}
}
All components are done
Em... still nothing come out.
I see. I need to add routes and links to App.js
.
./src/App.js
import React from 'react'
function App() {
return (
// Center the elements
<div style={{ textAlign: "center", width: "100%" }}>
<BrowserRouter>
<Link to="/hello">hello</Link>
<Link to="/world">world</Link>
<Route path="/hello" render={() => <div>hello</div>} />
<Route path="/world" render={() => <div>world</div>} />
</BrowserRouter>
</div>
);
}
export default App
Now my little "hello world" is done. Let's see the result.
Yay! It works!
Conclusion
react-router indeed is a very very solid middleware for React. After exploring the whole project, I can see that there are a lot of great efforts to make it as simple as possible. What I created myself was just a very basic stuff which is based on react-router v1.0.0 and it is only an example to help me understand how router works. Gotta keeps study and exploring so that I could improve and become a better engineer.
References
- https://reactrouter.com/
- https://github.com/ReactTraining/react-router
- https://knowbody.github.io/react-router-docs/
- https://stackoverflow.com/questions/42123261/programmatically-navigate-using-react-router-v4
- https://www.kirupa.com/react/creating_single_page_app_react_using_react_router.htm
- https://reactjs.net/features/react-router.html
Originally on pitayan.com
Top comments (0)