This article was originally published on bmf-tech.com.
Overview
Preparation
First, let's understand the History API. GO TO MDN.
For those in a hurry, just understanding pushState and window.popstate should suffice.
Specifications
This router will support the following URLs:
/post/post/:id/post/:id/:title
It does not support query parameters.
Packages Used
We will skip the React-related packages.
There is only one package used besides React:
This package helps with regular expressions for the URL part.
I would like to write my own regular expressions eventually, but for now, I will rely on this package.
Implementation
Create Components for Navigation and Pages
Prepare components corresponding to navigation and the pages.
src/
├── App.js
├── Dashboard.js
├── Home.js
├── Post.js
└── Profile.js
Implement Routing
Now, let's implement the routing.
We will prepare two components: Router and Route.
Router is the component that handles rendering based on the URL.
Route is just a component that wraps an anchor tag.
We will also prepare a file called routes.js to describe the routing conventions.
routes.js is an array of objects that describes the paths and their corresponding components.
At this point, you might have an idea of the overall routing process:
Initial State (First View)
- Get the current URL information.
- Render the component that matches the current URL information.
The URL information is held as State.
Transition
- Get the path of the clicked link.
- Add to history and transition using the History API's
pushState. - Re-render the component.
The State is updated, and the component is re-rendered.
The implementation of each component looks like this:
Route.js
import React, {Component} from 'react';
const history = window.history;
class Route extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
event.preventDefault();
const info = {
'url': event.target.href,
'path': event.target.pathname
};
this.handlePush(info.url);
this.props.handleRoute(info);
}
handlePush(url) {
// Create a history, and transition to next url
history.pushState(null, null, url);
}
render() {
return (<React.Fragment>
<a href={this.props.path} onClick={this.handleClick}>{this.props.text}</a>
</React.Fragment>);
}
}
export default Route;
Router.js
import React, {Component} from 'react';
import toRegex from 'path-to-regexp';
class Router extends Component {
handleComponent() {
const routes = this.props.routes;
const info = this.props.info;
for (const route of routes) {
const keys = [];
const string = new String(route.path);
const pattern = toRegex(string, keys);
const match = pattern.exec(info.path);
if (!match) {
continue;
}
const params = Object.create(null);
for (let i = 1; i < match.length; i++) {
params[keys[i - 1].name] = match[i] !== undefined
? match[i]
: undefined;
}
if (match) {
return route.action(Object.assign(info, {"params": params}));
}
}
return 'Not Found';
}
render() {
return (this.handleComponent());
}
}
export default Router;
routes.js
import React, {Component} from "react";
import Home from "./Home";
import Dashboard from "./Dashboard";
import Profile from "./Profile";
import Post from "./Post";
const HomeComponent = (params) => (<Home {...params}/>);
const DashboardComponent = (params) => (<Dashboard {...params}/>);
const ProfileComponent = (params) => (<Profile {...params}/>);
const PostComponent = (params) => (<Post {...params}/>);
export const routes = [
{
path: "/",
action: HomeComponent
}, {
path: "/dashboard",
action: DashboardComponent
}, {
path: "/profile",
action: ProfileComponent
}, {
path: "/post/:id",
action: PostComponent
}
];
App.js
import React, {Component} from 'react';
import Router from './Router';
import Route from './Route';
import {routes} from './routes';
class App extends Component {
constructor(props) {
super(props);
this.state = {
'url': '', // current url
'path': '' // current path
};
this.handleRoute = this.handleRoute.bind(this);
}
handleRoute(info) {
// Update url info
this.setState(info);
}
render() {
return (<React.Fragment>
<p>Current URL: {this.state.url}</p>
<p>Current Path: {this.state.path}</p>
{/* Navigation */}
<ul>
<li>
<Route path="/" text="Top" handleRoute={this.handleRoute}/>
</li>
<li>
<Route path="/dashboard" text="Dashboard" handleRoute={this.handleRoute}/>
</li>
<li>
<Route path="/profile" text="Profile" handleRoute={this.handleRoute}/>
</li>
<li>
<Route path="/post/9" text="Post-Id" handleRoute={this.handleRoute}/>
</li>
</ul>
{/* Router Component */}
<Router routes={routes} info={this.state}/>
</React.Fragment>);
}
}
export default App;
The strange line breaks in jsx are probably due to not properly configuring eslint...
I referred to You might not need React Router quite a bit.
The challenging part of the implementation was figuring out how to retrieve and maintain the information of parameters (like :id), but thanks to the awesome library path-to-regexp, I was able to overcome that.
Github
Here is the source code for this project.
It is also published on npm.
Thoughts
I feel like it could be cleaner if I used EventEmitter or Observer... (lack of study)
References
Reference Articles
- You might not need React Router
- Building a React-based Application
- Routing in React, the uncomplicated way
- MDN - History
- MDN - Manipulating the Browser History
- Trying out the History API
- Notes on manipulating URLs with JavaScript
Top comments (0)