preface
It took some time to sort out the react router system, including the functional principles and basic implementation methods of common components. The code posted in this paper is the implementation of the core principles of each component, which will be slightly different from the source code. Please note that the source code address has provided a detailed connection. Click to jump. Rest assured.
rendering method
- children
- component
- render
Priority:
These three rendering methods are mutually exclusive. When they exist at the same time: children > component > render;
This is the code about priority in the source code;
matters needing attention
- children and render can only pass in the nodes to be displayed in the form of anonymous functions, while component is not required.
- component and render can only be displayed after the path is matched, and children will be displayed whether they are matched or not.
- component is not recommended to pass in the node to be displayed in the form of anonymous function, because React.createElement will be called during rendering. If anonymous function is used, a new type will be generated every time, resulting in frequent mounting and unloading of sub components, and children and render will not;
If you are interested, you can try to run the code;
'use strict'; import React, { useState, useEffect } from 'react'; import { Router, Route } from 'react-router'; const Child = (props) => { useEffect(() => { console.log("mount "); return () => console.log("uninstall"); }, []); return <div>Child - {props.count}</div> } class ChildFunc extends React.Component { componentDidMount() { console.log("componentDidMount"); } componentWillUnmount() { console.log("componentWillUnmount"); } render() { return <div> ChildFunc - {this.props.count} </div> } } const Index = (props) => { const [count, setCount] = useState(0); return <div> <button onClick={() => setCount((state) => state + 1)}>add</button> <p>chick change count{count}</p> <Router > {/* bad Look at the log of the mount and unload functions*/} <Route component={() => <Child count={count} />} /> <Route component={() => <ChildFunc count={count} />} /> {/* good This is the correct way to open. Observe the log of mounting and unloading functions*/} <Route render={() => <Child count={count} />} /> <Route render={() => <ChildFunc count={count} />} /> {/* It's OK to observe the log of mounting and unloading functions, but children don't need to match the path. Use it with caution*/} <Route children={() => <ChildFunc count={count} />} /> <Route children={() => <Child count={count} />} /> </Router> </div> }; export default Index;
Link assembly
link is essentially an a tag, but when clicking directly with the href attribute, there will be jitter. You need to jump by using the command. Some attributes and functions are added to it in the source code, and the parameter to and click events are processed.
Source code Please move
'use strict'; import React, { useContext } from 'react' import RouterContext from './RouterContext' export default function Link({ to, children }) { const { history } = useContext(RouterContext) const handle = e => { // Prevent jitter, so disable the default behavior and jump in the form of command e.preventDefault(); history.push(to) }; return <a href={to} onClick={handle}>{children}</a> };
BrowserRouter component
This component is the uppermost component of the react router. It is mainly used to determine which route the routing system uses.
View source code Please move
'use strict' import React, { PureComponent } from 'react'; import { createBrowserHistory } from 'history' import Router from "./Router" export default class BrowserRouter extends PureComponent { constructor(props) { super(props); this.history = createBrowserHistory(); } render() { return <Router history={this.history}>{this.props.children}</Router> } };
RouterContext.js file
Because the routing component can be nested with ordinary element nodes and can not well determine the specific hierarchical relationship, we still choose the cross hierarchical data enemy method to implement it. Declaring and exporting RouterContext into separate files will make the logic clearer.
The source code does not directly use createContext, but packages a layer of createNamedContext and adds a displayName to the generated context
'use strict'; import React from 'react' const RouterContext = React.createContext(); export default RouterContext;
Router.js file
Main functions of Router file:
- Pass down history, location, match and other attributes through RouterContext;
- The location of the page is monitored through history.listen, and the location is passed down to facilitate the Route component and Switch component to match;
Source code
'use strict' import React, { PureComponent } from 'react'; import RouterContext from 'RouterContext' export default class Router extends PureComponent { static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; } constructor() { super(props) this.state = { location: props.history.location } this.unlinsten = props.history.listen((location) => { this.setState({ location }) }) } componentWillUnmount() { this.unlinsten(); } render() { return ( <RouterContext.Provider value={{ history: this.props.history, location: this.state.location, match: Router.computeRouteMatch(this.state.location.pathname) }} > {this.props.children} </RouterContext.Provider> ) } }
Route component
The route component is mainly responsible for processing the match and returning the component component to be rendered. This match can be the computedMatch passed down from the upper Switch component. If the upper Switch component is not used, it will determine whether the path attribute received by the route component exists. This will be compared with location.pathname. If the match is displayed, it will not be displayed, Path can also be empty. If it is empty, use context.match directly;
'use strict' import React, { PureComponent } from 'react'; import matchPath from './matchPath'; import RouterContext from './RouterContext'; export default class Route extends PureComponent { render() { return <RouterContext.Consumer > {(context) => { const { path, children, component, render, computedMatch } = this.props; const { location } = context; // When match, it indicates that the current match is successful const match = computedMatch ? computedMatch : path ? matchPath(location.pathname, this.props) : context.match; const props = { ...context, match } // After the matching is successful, it should be rendered according to the priority of children > component > render return <RouterContext.Provider value={props}> { match ? children ? typeof children === "function" ? children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? children(props) : null } </RouterContext.Provider> }} </RouterContext.Consumer> } }
be careful:
- The match repeatedly mentioned in the above code is the match of our route mount parameters;
- We wrap a layer of RouterContext.Provider around the return value in the component of human return. The reason is that when we use useRouteMatch and useParams to obtain the match externally, the match obtained by the context is actually the initial value passed down from the Router.js file, but we need to obtain the match value in the Route component, so we need to wrap it on the first layer, Here, the nearest value of context is used;
switch component
Switch means exclusive route. Its function is to match routes and render only the first route or redirect matched;
For the above reasons, components such as 404 that do not write the path attribute must be placed last, otherwise once the 404 component is matched, the subsequent sub components will not match again;
The difference between the Switch and the Route component is that the Switch controls which Route component is displayed, and the Route component is empty, which is whether the component under the current Route component is displayed
'use strict' import React, { PureComponent } from 'react'; import matchPath from './matchPath'; import RouterContext from './RouterContext'; export default class Switch extends PureComponent { render() { return <RouterContext.Consumer> { (context) => { let match; // Does the tag match let element; // Matched elements /** * There may be one or more props.children received here * Theoretically, we need to make if judgment by ourselves, but React provides an api,React.Children * forEach in it will help us accomplish such things */ React.Children.forEach(this.props.children, child => { // isValidElement determines whether it is a React node if (match == null && React.isValidElement(child)) { element = child; match = child.props.path ? matchPath(context.location.pathname, child.props) : context.match } }); return match ? React.cloneElement(element, { computedMatch: mactch }) : null } } </RouterContext.Consumer> } }
redirect
redirect is a route redirection. It is used to:
- Returns an empty component.
- Jump to execution page
'use strict' import React, { useEffect, PureComponent } from 'react'; import RouterContext from './RouterContext'; export default class Redirect extends PureComponent { render() { return <RouterContext.Consumer> { context => { const { history } = context; const { to } = this.props; return <LifeCycle onMount={() => history.push(to)} /> } } </RouterContext.Consumer> } } const LifeCycle = ({ onMount }) => { useEffect(() => { if (onMount) onMount() }, []) return null }
Several commonly used hook s
Post the code directly. I won't describe these simple.
import RouterContext from "./RouterContext"; import {useContext} from "react"; 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 : {}; }
withRouter doesn't need to be written. It's simple to set a high-level component, get the next context, and then pass it in.
summary
The knowledge points are basically written above. Here is a brief summary:
- The BrowserRouter component determines what type of history the routing system uses at the top layer;
- Then define the context in the Router file, pass the attributes such as history, match and loaction through cross level communication, and use history.listen to monitor the changes of loaction;
- Compare the path and location in the Router component and the Switch component, and render the corresponding components. The Switch component determines which Route component to render, and the Route component determines whether the current component is rendered;
- There are three rendering methods for Route components, which are mutually exclusive and children > component > render. It should be noted that the input criteria of the three attributes and the anonymous function method of component are not recommended;
- Another point to note in Route is to enable us to accurately obtain matches in subsequent use. Here, when return ing, we need to wrap it with < routercontext. Provider value = {props} > < / routercontext. Provider > and pass in a new match, as well as the nearest value retrieval feature of context;
- Switch component means exclusive Route, that is, only the first Route component matched is rendered;