React Router Navigation guard

Navigation guard

As we know, Vue provides several hook functions that allow us to accomplish the navigation guard function. Global beforeEach and afterEach, internal hook functions beforeRouteEnter, BeforeRouteUpdate and beforeRouteLeave, so we can do things like verify permissions or change TAB titles before or after a route jump

The React Router only provides several components to redirect and display content, but does not provide such hook functions. Therefore, we need to implement this solution by ourselves

We need to wrap a routing guard component and get the History object via withRouter or useHistory

The history object has a listen function: it adds a listener that listens for address changes, and when the address changes, the function passed to it is called

  • The runtime point of this function is:Just before jumping to a new page
  • The function passed takes two arguments:Location object 和 Jump mode ('POP'/'PUSH'/'REPLACE')
  • calllistenThe return value of the function is a function that cancels listening for route changes

Jump mode (action) :

  • 'POP': out of the stack; (byClick browser forward/Back, the callhistory.go() / history.goBack() / history.goForward()Jump)
  • 'PUSH': into the stack; (callhistory.push()Jump)
  • 'REPLACE': into the stack; (callhistory.replace()Jump)
import {PureComponent} from 'react' import {withRouter} from 'react-router-dom' class GuardRouter extends PureComponent { componentDidMount() { this.unListen = this.props.history.listen((location, Action) => {console.log(location) // location object console.log(action) // 'POP'/'PUSH'/'REPLACE' const prevLocation = This.props. Location // The convention property onRouterChange passes a function, When the page jump handle some things this. Props. OnRouterChange && enclosing props. OnRouterChange (prevLocation, the location, action, ComponentWillUnmount () {componentWillUnmount() {this.props. Children}} export default withRouter(GuardRouter)Copy the code
// Example usage:  import React from 'react' import {BrowserRouter as Router} from 'react-router-dom' import GuardRouter from './components/GuardRouter' import Home from './views/Home' import News from './views/News' import AboutUs from './views/AboutUs' let count = 0 function routerChange(prevLocation, curLocation, action, UnListen) {count ++ console.log(' log ${count} : ${prevLocation. Pathname} - > ${curLocation. Pathname}. If (count === 5) unListen()} export default function App() {return (<Router> <Link to='/'> home </Link> <Link to='/news'> </Link> <Link to='/aboutus'> </Link> <GuardRouter onRouterChange={routerChange}> <Route path='/' component={Home} /> <Route path='/news' component={News} /> <Route path='/aboutus' component={AboutUs} /> </GuardRouter>  </Router>) }Copy the code

At this point, we have been able to listen to the change of the route jump, and we can also get the information about the route jump, but there is still a lot of difference with the Vue route guard, because we still cannot block its route jump……

Then we need to set a block, and only if this.props.history. Block is set to block (if multiple blocks are set to a warning and only the last block is passed), The Router component’s getUserConfirmation property will run, and the blocking message set by the block will be passed as the first argument to getUserConfirmation. The second argument is a callback function. Callback (true)/callback(false)

The history.block function takes a single argument, either a string or a function that returns a string (which is run on every jump). This string function can take two arguments, just like listen (which takes arguments: The location and the action)

// GuardRouter class GuardRouter extends PureComponent { componentDidMount() { this.unListen = this.props.history.listen((location, action) => { // add listen... } // Set block this.props.history.block(' Set block info: Sure to jump to page? ') } componentWillUnmount() { this.unListen() } render() { return this.props.children } } export default WithRouter (GuardRouter) // App export default function App() {const getConfirm = useCallback((MSG, Callback) => {console.log(MSG) // print: 'Set blocking information: sure to jump to page? '// callback(true) // skip // callback(false) // skip}, []) return (<Router getUserConfirmation={getConfirm}> {/* Link and Route configuration */} </Router>)}Copy the code

Interceptor level increased

From the above example, we can see that setting block and getUserConfirmation is separate, which is obviously not a good use of encapsulation, so we need to upgrade our encapsulation of GuardRouter to replace the Router root component:

Import {BrowserRouter as Router} from 'react-router-dom' Only used to set blocking */ class _GuardRouterHelper extends PureComponent {componentDidMount() {// Sets blocking this.props.history.block((location, Return ""})} render() {return null}} const GuardRouterHelper = withRouter(_GuardRouterHelper) // This component is not in the context of the Router and cannot get the history object. Class GuardRouter extends PureComponent {handleRouterConfirm = (MSG, next) => {// Convention handler before route jump onBeforeEach, Give the jump to the outer caller this.props. OnBeforeEach && this.props. OnBeforeEach (next)} render() {return <Router getUserConfirmation={this.handleRouterConfirm}> <GuardRouterHelper /> {this.props.children} </Router> } } export default GuardRouter // Export directlyCopy the code

Use the sample

import React from 'react' import {BrowserRouter as Router} from 'react-router-dom' import GuardRouter from './components/GuardRouter' import Home from './views/Home' import News from './views/News' import AboutUs from './views/AboutUs' function beforeRouterChange(next) {next(true) // Next (false) // }} export default function App() {return (<GuardRouter onBeforeEach={beforeRouterChange}> <Link to='/'> <Link to='/news'> news page </Link> <Link to='/aboutus'> aboutus </Link> <div> <Route path='/' component={Home} /> <Route path='/news' component={News} /> <Route path='/aboutus' component={AboutUs} /> </div> </GuardRouter>) }Copy the code

Another promotion

With the Settings block above, the argument can be a function that takes the location object and the action, so we can pass this information to the onBeforeEach function. Location object to call to path, jump method and jump control method), then there are two options:

  1. PrevLocation, location, Action, and unBlock are received using global variables (since the history object is a common object in the route and there is only one set block, there is no problem storing global variables) :

    import {BrowserRouter as Router} from 'react-router-dom' let prevLocation, curLocation, curAction, UnBlock Class _GuardRouterHelper extends PureComponent {componentDidMount() {// Sets the block ===============================================> unBlock = this.props.history.block((location, Action) => {prevLocation = this.props. Location curLocation = location curAction = action return "}) // Set block ===============================================> } render() { return null } } const GuardRouterHelper = WithRouter (_GuardRouterHelper) class GuardRouter extends PureComponent {/* Passes global variables in turn ======================================> */ handleRouterConfirm = (msg, next) => { this.props.onBeforeEach && this.props.onBeforeEach(prevLocation, curLocation, curAction, Next)} / * transfer global variables in turn out = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = > * / render () {return "the Router getUserConfirmation={this.handleRouterConfirm}> <GuardRouterHelper /> {this.props.children} </Router> } } export default GuardRouter // Export directlyCopy the code
  2. If there are no special variables for these variables, the JSON. Stringify function can be formatted to return a JSON string, which is received by the MSG variable of the listener function:

    import {BrowserRouter as Router} from 'react-router-dom' class _GuardRouterHelper extends PureComponent { ComponentDidMount () {/ / set the blocked = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = > this. Props. History. The block (the location, MSG return json.stringify ({const prevLocation = this.props. Location) => {const prevLocation = this.props. prevLocation, location, Action})}) / / set the blocked = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = >} render () {return null}} const GuardRouterHelper = withRouter(_GuardRouterHelper) class GuardRouter extends PureComponent {/* Parses JSON with json.parse String = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = > * / handleRouterConfirm = (MSG, next) = > {const {prevLocation, location, action } = JSON.parse(msg) this.props.onBeforeEach && this.props.onBeforeEach(prevLocation, location, action, Next)} / * using JSON parse parsing JSON string = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = > * / render () {return "the Router getUserConfirmation={this.handleRouterConfirm}> <GuardRouterHelper /> {this.props.children} </Router> } } export default GuardRouter // Export directlyCopy the code

Unblocker

As with adding a route listener, history.block returns an unblocking function. To pass this unblocking function to onBeforeEach, you have to store it as a global variable and pass it as an argument

class _GuardRouterHelper extends PureComponent { componentDidMount() { this.unBlock = this.props.history.block((location, Action) => {// add block})} componentWillUnmount() {this.unblock () // unBlock} render() {return null}} const GuardRouterHelper = withRouter(_GuardRouterHelper)Copy the code

The final code

The previous listener works with a blocker to accomplish route interception:

import {BrowserRouter as Router} from 'react-router-dom' let unBlock class _GuardRouterHelper extends PureComponent { componentDidMount() { unBlock = this.props.history.block((location, action) => { const prevLocation = this.props.location return JSON.stringify({ prevLocation, location, action }) }) this.unListen = this.props.history.listen((location, Action) => {const prevLocation = this.props. Location // Convention property onRouterChange passes a function, When the page jump handle some things this. Props. OnRouterChange && enclosing props. OnRouterChange (prevLocation curlocation, action, ComponentWillUnmount () {componentWillUnmount() {componentWillUnmount() {return null}}  const GuardRouterHelper = withRouter(_GuardRouterHelper) class GuardRouter extends PureComponent { handleRouterConfirm = (msg, next) => { const { prevLocation, location, action } = JSON.parse(msg) if (this.props.onBeforeEach) { this.props.onBeforeEach(prevLocation, location, action, next, UnBlock)} else {next(true)} render() {return <Router getUserConfirmation={this.handlerOuterConfirm}> <GuardRouterHelper onRouterChange={this.props.onRouterChange} /> {this.props.children} </Router> } } export default Function beforeRouterChange(prevLocation, curLocation, action, next, UnBlock) {if (/* meets the jump condition */) {next(true) // jump route} else {next(false) // do not jump route} // unBlock() // unBlock} // Route changes run the function function handleRouterChange(prevLocation, curlocation, action, Export default function App() {return (<GuardRouter) {// unListen() // Cancel route redirect listener} export default function App() {return (<GuardRouter) OnBeforeEach ={beforeRouterChange} onRouterChange={handleRouterChange} > <Link to='/'> home </Link> <Link To ='/news'> news page </Link> <Link to='/aboutus'> aboutus </Link> <div> <Route path='/' component={Home} /> <Route path='/news' component={News} /> <Route path='/aboutus' component={AboutUs} /> </div> </GuardRouter> ) }Copy the code