React -router-dom use and source code implementation
React-router-dom is a routing solution specifically for Web applications.
Router, Link, Route
1.1. Basic use
Let’s start with a simple example:
import React from "react";
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";
export default function App() {
return (
<div className="App">
<Router>
<Link to="/">Home page</Link>
<Link to="/user">The user center</Link>
<Link to="/login">The login</Link>
<Link to="/product/123">goods</Link>
<Route
path="/"
children={()= > HomePage({ data: "children" })}
component={HomePage}
render={() => <HomePage data="render" />} / ><Route path="/user" component={UserPage} />
<Route path="/login" component={LoginPage} />
</Router>
</div>
);
}
function HomePage(props) {
console.log("index", props);
return <div>Home - {props. Data}</div>;
}
function UserPage(props) {
return <div>The user center</div>;
}
function LoginPage(props) {
return <div>The login</div>;
}
Copy the code
The example uses the three components of React-router-DOM, BrowserRouter, Route, and Link.
A Router component is like a Router. Link refers to the network cable interface and network cable of the Router, and Route refers to various devices connected to the Router.
With routers, network cables and devices make sense, so the Route and Link components must be placed in the Router component.
The Link component’s to property specifies the Route to jump to when the Link is clicked, the Route component’s Path property specifies the Route that the Route component matches, If matched, render the component specified by the children, Component, and render attributes (all of which specify the component to render). The differences are as follows:
children
The specified component is rendered even if the route does not match
component
The specified component will only be rendered if the route matches. It is best not to use inline functions as this will result in a new component being generated and mounted on each re-rendering rather than updating on the previous component. If you want to use the form of an inline function, it is better to use render or children
render
Suitable for passing inline functions that are executed when a route is matched
Children > Component > render
1.2, source code implementation
To implement a routing-related component, we need to use a library history. This library provides a method createBrowserHistory that can be used to create a history object, store information such as route history, and listen for route changes.
1.2.1 Link component
Start with the simplest, that is the Link component, the essence of a Link component is an A tag
<Link to="/"> home page < / Link ><Link to="/user">The user center</Link>
<Link to="/login">The login</Link>
<Link to="/product/123">goods</Link>
Copy the code
import { useContext } from "react";
import RouterContext from "./RouterContext";
export default function Link(props) {
const { to, children } = props;
const context = useContext(RouterContext);
function onClick(e) {
// Prevent default behavior
e.preventDefault();
// Use the history object in the context passed by the Router component to change the route
context.history.push(to);
}
return (
<a href={to} onClick={onClick}>
{children}
</a>
);
}
Copy the code
The to attribute of the Link component is the href attribute of a tag, but the default behavior of clicking a tag will refresh the page, while our SPA application will not refresh the page when jumping to the route, so we need to add onClick method to a tag to prevent the default behavior. Then use the push method of the history object (where the history object is passed through the context). The context is passed from the Router component, which is why components such as Link and Route must be written to the Router component.
1.2.2 the Router components
Let’s look at the implementation of the Router component
import React, { useState, useEffect } from "react";
import RouterContext from "./RouterContext";
export default function Router(props) {
const { history } = props;
const [state, setState] = useState({ location: history.location });
function computeRootMatch(pathname) {
return { path: "/".url: "/".params: {}, isExact: pathname === "/" };
}
useEffect(() = > {
const unlisten = history.listen(({ location }) = > {
console.log("location", location);
setState({ location });
});
return () = > {
unlisten();
};
});
const routeObj = {
history: history,
location: state.location,
match: computeRootMatch(state.location.pathname)
};
return (
<RouterContext.Provider value={routeObj}>
{props.children}
</RouterContext.Provider>
);
}
Copy the code
The Router component has a history object in props, which is used to get the location information of the route and to listen for route changes.
The Router component also uses the Context to pass routing information such as history and location to the underlying components, so we need to create a Context object
import React from "react";
const RouterContext = React.createContext();
export default RouterContext;
Copy the code
When the Link component is clicked, the history.push method is called to add the Route address to the history object. Then the History object in the Route component can listen for Route changes and update the Route with setState. The Route component of the Router component also triggers an update. The Route component takes the latest Route address and matches it with its path property. If the match is successful, render the component specified by the Route component or render property. So the Route component implementation ideas are also generally available.
Before implementing the Route component, let’s look at the history on the Router component props.
There are several different types of router components in the React-router-DOM, such as BrowserRouter and HashRouter
Therefore, it can be inferred that these types of components are at the top of the Router component that pass the different types of history objects to the Router component.
BrowserRouter component implementation
import React from "react";
import { createBrowserHistory } from "history";
import Router from "./Router";
export default function BrowserRouter(props) {
const history = createBrowserHistory();
return <Router history={history}>{props.children}</Router>;
}
Copy the code
1.2.3 the Route component
Now you can implement the most critical Route component
import React, { useContext } from "react";
import { matchPath } from "react-router-dom";
import RouterContext from "./RouterContext";
export default function Route(props) {
const context = useContext(RouterContext);
const { path, children, component, render, computedMatch } = props;
const { location } = context;
const match = computedMatch
? computedMatch
: path
? matchPath(location.pathname, props)
: context.match; // location.pathname === path;
constrouteProps = { ... context, match };// 1. Match the children Component with the priority of render
// 2. Mismatch: call if children is function, render null otherwise
let result = null;
if (match) {
if (children) {
result = typeof children === "function" ? children(routeProps) : null;
} else if (component) {
result = React.createElement(component, routeProps);
} else if(render) { result = render(routeProps); }}else {
result = typeof children === "function" ? children(routeProps) : null;
}
return (
<RouterContext.Provider value={routeProps}>{result}</RouterContext.Provider>
);
}
Copy the code
We need to get the properties path, children, Component, and render on the Route component via props. Then we need to get the current location object from the context, which holds the current Route address. Then take the path and the current location information to match. Here we use a method matchPath provided by react-router-dom for convenience to match the route and return the match information
const match = matchPath('/users/123', {
path: '/users/:id'.exact: true.strict: false
})
// Return the match object in the following format:
{
path: '/users/:id'.url: '/user/123'.isExact: true.params: {
id: '123'}}Copy the code
Get the match object and render the components in the order children > Component > render.
This completes the basic react-router-dom.
To sum up:
1. The Router component passes the history object through the Context to the Link and Route components.
2. Click Link component to trigger update of history object;
3. The Route component listens for changes in the History object, compares the latest routing information with its own path, and if a match is found renders the component specified by the Component or Render property.
2, the Switch
2.1. Basic use
Placing the Route component in the Switch component renders only the first Route component that matches the Route address.
If it’s just a bunch of Route components, the about, User, and NoMatch components will all be rendered when the Route address is /about.
If wrapped with Switch, only the About component will be rendered.
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>
Copy the code
2.2 source code implementation
You can guess how the Switch might be implemented: walk through the Switch’s child components, find the first one that matches the current routing address, and render it.
import React, { useContext } from "react";
import { matchPath } from "react-router-dom";
import RouterContext from "./RouterContext";
export default function Switch(props) {
const { location, match } = useContext(RouterContext);
let computedMatch = null; // Flags match
let matchEle = null; // Marks the matched element
// Use react.children. ForEach to iterate over the Switch's Children
React.Children.forEach(props.children, (child) = > {
if (!computedMatch && React.isValidElement(child)) {
matchEle = child;
computedMatch = child.props.path
? matchPath(location.pathname, child.props)
: match;
}
});
let result = match ? React.cloneElement(matchEle, { computedMatch }) : null;
return result;
}
Copy the code
At the heart of the code above is the use of the react.children. ForEach method to traverse the Switch’s Children.
Because there are three possibilities for the props. Children value:
- There is no child node, is undefined
- A child node is an object
- Multiple child nodes, an array of objects
React provides this method to make it easy to iterate through child components. Once the first matching child component is found, clone it using react. cloneElement and pass the matching information (the match object returned by the matchPath method) to the component.
3, Redirect
3.1. Basic use
Render
to navigate to a new address. This new address overrides the current address in the history stack, similar to a server-side (HTTP 3XX) redirection.
<Link to="/user"> User center </Link><Link to="/login">The login</Link>
<Link to="/product/123">goods</Link>
<Link to="/redirect">redirect</Link>
<Route path="/user" component={UserPage} />
<Route
path="/redirect"
render={()= > <Redirect to="/user"></Redirect>} / >
Copy the code
When a link Redirect is clicked, the Route component is rendered, which actually renders the Redirect component and then navigates to a new address, /user, rendering the UserPage component.
3.2 source code implementation
import { useContext, useEffect } from "react";
import RouterContext from "./RouterContext";
export default function Redirect(props) {
const context = useContext(RouterContext);
const { history } = context;
const { to } = props;
return <LifeCycle onMount={()= > history.push(to)}></LifeCycle>;
}
function LifeCycle(props) {
useEffect(() = > {
props.onMount();
});
return null;
}
Copy the code
How do class components and function components get the history object
When we implemented the Route component earlier, we passed the context object to the component to be rendered, namely the history, location, and match objects.
if (match) {
if (children) {
console.log("children", children);
result = typeof children === "function" ? children(routeProps) : null;
console.log("result", result);
} else if (component) {
result = React.createElement(component, routeProps);
} else if(render) { result = render(routeProps); }}else {
result = typeof children === "function" ? children(routeProps) : null;
}
Copy the code
So we can get these three properties in the props of the rendered component, such as this.props. Location
But if we render a component with a child component, we can’t get these three objects in the child component, and we can’t use history.push for route jump and location.search for route parameters, unless the parent component passes these three attributes to the child component. But it’s too cumbersome to pass the child component every time. So react-router-dom gives us some methods to get the history route object directly:
4.1. Basic use
-
Class component, we can wrap our component using the withRouter high-order component, and then we can get objects like history in props.
class Detail extends Component { constructor(props) { super(props) console.log('Detail', props) } render() { return <div>Details page</div> } } Detail = withRouter(Detail) Copy the code
If the withRouter component is not wrapped, the Detail component props has no properties such as history
-
UseHistory, useLocation, useRouteMatch, useParams
function DetailFunc(props) { const history = useHistory() const location = useLocation() const match = useRouteMatch() const params = useParams() console.log('DetailFunc routeProps', { history, location, match, params }) console.log('DetailFunc', props) return <div>Functional component Detail</div> } Copy the code
4.2 source code implementation
1, withRouter
The essence is a function that takes a component as an argument and returns a new functional component. All it does is get the Context object and pass it to the input component.
import RouterContext from './RouterContext'
const withRouter = (WrappedComponent) = > (props) = > {
return (
<RouterContext.Consumer>
{(context) => {
return <WrappedComponent {. props} {. context} / >
}}
</RouterContext.Consumer>)}export default withRouter
Copy the code
2, hooks,
Hooks are simpler to implement by using useContext to retrieve the context and return the history, location, match, etc.
// useHistory,
// useLocation,
// useRouteMatch,
// useParams,
import { useContext } from 'react'
import RouterContext from './RouterContext'
export function useHistory() {
return useContext(RouterContext).history
}
export function useLocation() {
return useContext(RouterContext).location
}
export function useRouteMatch() {
return useContext(RouterContext).match
}
export function useParams() {
const match = useContext(RouterContext).match
return match ? match.params : {}
}
Copy the code
At this point, the basic functions of react-router-dom are complete.
Demo code: my-router-dom