Detecting user leaving page with react-router

ReactjsReact Router

Reactjs Problem Overview


I want my ReactJS app to notify a user when navigating away from a specific page. Specifically a popup message that reminds him/her to do an action:

> "Changes are saved, but not published yet. Do that now?"

Should i trigger this on react-router globally, or is this something that can be done from within the react page / component?

I havent found anything on the latter, and i'd rather avoid the first. Unless its the norm of course, but that makes me wonder how to do such a thing without having to add code to every other possible page the user can go to..

Any insights welcome, thanks!

Reactjs Solutions


Solution 1 - Reactjs

react-router v4 introduces a new way to block navigation using Prompt. Just add this to the component that you would like to block:

import { Prompt } from 'react-router'

const MyComponent = () => (
  <>
    <Prompt
      when={shouldBlockNavigation}
      message='You have unsaved changes, are you sure you want to leave?'
    />
    {/* Component JSX */}
  </>
)

This will block any routing, but not page refresh or closing. To block that, you'll need to add this (updating as needed with the appropriate React lifecycle):

componentDidUpdate = () => {
  if (shouldBlockNavigation) {
    window.onbeforeunload = () => true
  } else {
    window.onbeforeunload = undefined
  }
}

onbeforeunload has various support by browsers.

Solution 2 - Reactjs

In react-router v2.4.0 or above and before v4 there are several options

  1. Add function onLeave for Route

 <Route
      path="/home"
      onEnter={ auth }
      onLeave={ showConfirm }
      component={ Home }
    >
    

2. Use function setRouteLeaveHook for componentDidMount

You can prevent a transition from happening or prompt the user before leaving a route with a leave hook.

const Home = withRouter(
  React.createClass({

    componentDidMount() {
      this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave)
    },

    routerWillLeave(nextLocation) {
      // return false to prevent a transition w/o prompting the user,
      // or return a string to allow the user to decide:
      // return `null` or nothing to let other hooks to be executed
      //
      // NOTE: if you return true, other hooks will not be executed!
      if (!this.state.isSaved)
        return 'Your work is not saved! Are you sure you want to leave?'
    },

    // ...

  })
)

Note that this example makes use of the withRouter higher-order component introduced in v2.4.0.

However these solution doesn't quite work perfectly when changing the route in URL manually

In the sense that

  • we see the Confirmation - ok
  • contain of page doesn't reload - ok
  • URL doesn't changes - not okay

For react-router v4 using Prompt or custom history:

However in react-router v4 , its rather easier to implement with the help of Prompt from'react-router

According to the documentation

> Prompt > > Used to prompt the user before navigating away from a page. When your > application enters a state that should prevent the user from > navigating away (like a form is half-filled out), render a <Prompt>. > > import { Prompt } from 'react-router' >
> when={formIsHalfFilledOut} > message="Are you sure you want to leave?" > /> > > message: string > > The message to prompt the user with when they try to navigate away. > > > > message: func > > > Will be called with the next location and action the user is > attempting to navigate to. Return a string to show a prompt to the > user or true to allow the transition. > > ( > Are you sure you want to go to ${location.pathname}? > )}/> > > when: bool > > Instead of conditionally rendering a <Prompt> behind a guard, you > can always render it but pass when={true} or when={false} to > prevent or allow navigation accordingly.

In your render method you simply need to add this as mentioned in the documentation according to your need.

UPDATE:

In case you would want to have a custom action to take when user is leaving page, you can make use of custom history and configure your Router like

history.js

import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()

... 
import { history } from 'path/to/history';
<Router history={history}>
  <App/>
</Router>

and then in your component you can make use of history.block like

import { history } from 'path/to/history';
class MyComponent extends React.Component {
   componentDidMount() {
      this.unblock = history.block(targetLocation => {
           // take your action here     
           return false;
      });
   }
   componentWillUnmount() {
      this.unblock();
   }
   render() {
      //component render here
   }
}

Solution 3 - Reactjs

For react-router 2.4.0+

NOTE: It is advisable to migrate all your code to the latest react-router to get all the new goodies.

As recommended in the react-router documentation:

One should use the withRouter higher order component:

> We think this new HoC is nicer and easier, and will be using it in > documentation and examples, but it is not a hard requirement to > switch.

As an ES6 example from the documentation:

import React from 'react'
import { withRouter } from 'react-router'

const Page = React.createClass({

  componentDidMount() {
    this.props.router.setRouteLeaveHook(this.props.route, () => {
      if (this.state.unsaved)
        return 'You have unsaved information, are you sure you want to leave this page?'
    })
  }

  render() {
    return <div>Stuff</div>
  }

})

export default withRouter(Page)

Solution 4 - Reactjs

For react-router v3.x

I had the same issue where I needed a confirmation message for any unsaved change on the page. In my case, I was using React Router v3, so I could not use <Prompt />, which was introduced from React Router v4.

I handled 'back button click' and 'accidental link click' with the combination of setRouteLeaveHook and history.pushState(), and handled 'reload button' with onbeforeunload event handler.

setRouteLeaveHook (doc) & history.pushState (doc)

  • Using only setRouteLeaveHook was not enough. For some reason, the URL was changed although the page remained the same when 'back button' was clicked.

        // setRouteLeaveHook returns the unregister method
        this.unregisterRouteHook = this.props.router.setRouteLeaveHook(
          this.props.route,
          this.routerWillLeave
        );
    
        ...
        
        routerWillLeave = nextLocation => {
          // Using native 'confirm' method to show confirmation message
          const result = confirm('Unsaved work will be lost');
          if (result) {
            // navigation confirmed
            return true;
          } else {
            // navigation canceled, pushing the previous path
            window.history.pushState(null, null, this.props.route.path);
            return false;
          }
        };
    

onbeforeunload (doc)

  • It is used to handle 'accidental reload' button

      window.onbeforeunload = this.handleOnBeforeUnload;
     
      ...
    
      handleOnBeforeUnload = e => {
        const message = 'Are you sure?';
        e.returnValue = message;
        return message;
      }
    

Below is the full component that I have written

  • note that withRouter is used to have this.props.router.

  • note that this.props.route is passed down from the calling component

  • note that currentState is passed as prop to have initial state and to check any change

      import React from 'react';
      import PropTypes from 'prop-types';
      import _ from 'lodash';
      import { withRouter } from 'react-router';
      import Component from '../Component';
      import styles from './PreventRouteChange.css';
    
      class PreventRouteChange extends Component {
        constructor(props) {
          super(props);
          this.state = {
            // initialize the initial state to check any change
            initialState: _.cloneDeep(props.currentState),
            hookMounted: false
          };
        }
    
        componentDidUpdate() {
    
         // I used the library called 'lodash'
         // but you can use your own way to check any unsaved changed
          const unsaved = !_.isEqual(
            this.state.initialState,
            this.props.currentState
          );
    
          if (!unsaved && this.state.hookMounted) {
            // unregister hooks
            this.setState({ hookMounted: false });
            this.unregisterRouteHook();
            window.onbeforeunload = null;
          } else if (unsaved && !this.state.hookMounted) {
            // register hooks
            this.setState({ hookMounted: true });
            this.unregisterRouteHook = this.props.router.setRouteLeaveHook(
              this.props.route,
              this.routerWillLeave
            );
            window.onbeforeunload = this.handleOnBeforeUnload;
          }
        }
    
        componentWillUnmount() {
          // unregister onbeforeunload event handler
          window.onbeforeunload = null;
        }
    
        handleOnBeforeUnload = e => {
          const message = 'Are you sure?';
          e.returnValue = message;
          return message;
        };
    
        routerWillLeave = nextLocation => {
          const result = confirm('Unsaved work will be lost');
          if (result) {
            return true;
          } else {
            window.history.pushState(null, null, this.props.route.path);
            if (this.formStartEle) {
              this.moveTo.move(this.formStartEle);
            }
            return false;
          }
        };
    
        render() {
          return (
            <div>
              {this.props.children}
            </div>
          );
        }
      }
    
      PreventRouteChange.propTypes = propTypes;
    
      export default withRouter(PreventRouteChange);
    

Please let me know if there is any question :)

Solution 5 - Reactjs

Using history.listen

For example like below:

In your component,

componentWillMount() {
    this.props.history.listen(() => {
      // Detecting, user has changed URL
      console.info(this.props.history.location.pathname);
    });
}

Solution 6 - Reactjs

For react-router v0.13.x with react v0.13.x:

this is possible with the willTransitionTo() and willTransitionFrom() static methods. For newer versions, see my other answer below.

From the react-router documentation:

> You can define some static methods on your route handlers that will be called during route transitions. > > willTransitionTo(transition, params, query, callback) > > Called when a handler is about to render, giving you the opportunity to abort or redirect the transition. You can pause the transition while you do some asynchonous work and call callback(error) when you're done, or omit the callback in your argument list and it will be called for you. > > willTransitionFrom(transition, component, callback) > > Called when an active route is being transitioned out giving you an opportunity to abort the transition. The component is the current component, you'll probably need it to check its state to decide if you want to allow the transition (like form fields). > > Example > > var Settings = React.createClass({ > statics: { > willTransitionTo: function (transition, params, query, callback) { > auth.isLoggedIn((isLoggedIn) => { > transition.abort(); > callback(); > }); > }, > > willTransitionFrom: function (transition, component) { > if (component.formHasUnsavedData()) { > if (!confirm('You have unsaved information,'+ > 'are you sure you want to leave this page?')) { > transition.abort(); > } > } > } > } > > //... > });


For react-router 1.0.0-rc1 with react v0.14.x or later:

this should be possible with the routerWillLeave lifecycle hook. For older versions, see my answer above.

From the react-router documentation:

> To install this hook, use the Lifecycle mixin in one of your route components. > > import { Lifecycle } from 'react-router' > > const Home = React.createClass({ > > // Assuming Home is a route component, it may use the > // Lifecycle mixin to get a routerWillLeave method. > mixins: [ Lifecycle ], > > routerWillLeave(nextLocation) { > if (!this.state.isSaved) > return 'Your work is not saved! Are you sure you want to leave?' > }, > > // ... > > })

Things. may change before the final release though.

Solution 7 - Reactjs

You can use this prompt.

import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link, Prompt } from "react-router-dom";

function PreventingTransitionsExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Form</Link>
          </li>
          <li>
            <Link to="/one">One</Link>
          </li>
          <li>
            <Link to="/two">Two</Link>
          </li>
        </ul>
        <Route path="/" exact component={Form} />
        <Route path="/one" render={() => <h3>One</h3>} />
        <Route path="/two" render={() => <h3>Two</h3>} />
      </div>
    </Router>
  );
}

class Form extends Component {
  state = { isBlocking: false };

  render() {
    let { isBlocking } = this.state;

    return (
      <form
        onSubmit={event => {
          event.preventDefault();
          event.target.reset();
          this.setState({
            isBlocking: false
          });
        }}
      >
        <Prompt
          when={isBlocking}
          message={location =>
            `Are you sure you want to go to ${location.pathname}`
          }
        />

        <p>
          Blocking?{" "}
          {isBlocking ? "Yes, click a link or the back button" : "Nope"}
        </p>

        <p>
          <input
            size="50"
            placeholder="type something to block transitions"
            onChange={event => {
              this.setState({
                isBlocking: event.target.value.length > 0
              });
            }}
          />
        </p>

        <p>
          <button>Submit to stop blocking</button>
        </p>
      </form>
    );
  }
}

export default PreventingTransitionsExample;

Solution 8 - Reactjs

That's how you can show a message when user switch to another route or leave current page and go to another URL

import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
import { Prompt } from 'react-router-dom'
import { useTranslation } from 'react-i18next'


const LeavePageBlocker = ({ when }) => {
  const { t } = useTranslation()
  const message = t('page_has_unsaved_changes')

  useEffect(() => {
    if (!when) return () => {}

    const beforeUnloadCallback = (event) => {
      event.preventDefault()
      event.returnValue = message
      return message
    }

    window.addEventListener('beforeunload', beforeUnloadCallback)
    return () => {
      window.removeEventListener('beforeunload', beforeUnloadCallback)
    }
  }, [when, message])

  return <Prompt when={when} message={message} />
}

LeavePageBlocker.propTypes = {
  when: PropTypes.bool.isRequired,
}

export default LeavePageBlocker

Your page:

const [dirty, setDirty] = setState(false)
...
return (
  <>
    <LeavePageBlocker when={dirty} />
    ...
  </>
)

Solution 9 - Reactjs

May be you can use componentWillUnmount() to do anything before the user leaving the page. If you are using functional components, then you can just do the same with useEffect() hook. The hook accepts a function that returns a Destructor, which is similar to what componentWillUnmount() can do.

Credit goes to this article

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBarry StaesView Question on Stackoverflow
Solution 1 - ReactjsjcadyView Answer on Stackoverflow
Solution 2 - ReactjsShubham KhatriView Answer on Stackoverflow
Solution 3 - ReactjsactivatedgeekView Answer on Stackoverflow
Solution 4 - ReactjsSang Yun ParkView Answer on Stackoverflow
Solution 5 - ReactjsDebabrata GhoshView Answer on Stackoverflow
Solution 6 - ReactjsBarry StaesView Answer on Stackoverflow
Solution 7 - ReactjsJaskaran SinghView Answer on Stackoverflow
Solution 8 - ReactjsEugene CharniauskiView Answer on Stackoverflow
Solution 9 - Reactjsquantum.snowballView Answer on Stackoverflow