Blocking page navigation in ReactJS (React Router)

source

This article originally appeared on the Message Media Developer Blog, be sure to check it out.

Our product owner came to us with an interesting problem the other day, in the latest release of the Message Media Front End we have a new payment portal for customers to pay invoices, when a user is in the process of paying a bill we need to block navigation, or in his words:

As a billing contact I want to confirm my intent to leave the payment portal when my payment is still processing So that I am aware that my payment hasn't finished processing

Blocking page navigation with React Router

There seems to be quite a few methods of achieving this but most are pre v4…

Let’s go to the docs

React Router have a great online documentation portal available, upon a quick search we find the <Prompt /> method.

Prompt the user before navigating away from a page.

Great! Let’s create a simple component to test.

Simple Navigation Blocker Component

import React from 'react'
import PropTypes from 'prop-types'
import { Prompt } from 'react-router-dom'

const NavigationBlocker = props => (
  <Prompt when={props.navigationBlocked} message="Are you sure you want to leave?" />
)

NavigationBlocker.propTypes = {
  navigationBlocked: PropTypes.bool.isRequired,
}

export default NavigationBlocker

Note: I’m specifically using react-router-dom but you can use plain ol’ react-router for this

Users should now see a prompt when clicking on links between routes but what about browser actions eg. Back / Forward buttons, Reload or Close?

Blocking browser navigation with onbeforeunload

Once again you’re going to find many articles / stack overflow questions / tweets on how to implement this so I’d suggest

Go back to the docs

As per the MDN Documentation:

The WindowEventHandlers.onbeforeunload event handler property contains the code executed when the beforeunload is sent.

The problem is there’s many ways to implement this but as of 2017 most browsers block custom messages (for security reasons), as we don’t need to worry about this we can use something as simple as:

// Enable navigation prompt
window.onbeforeunload = () => true
// Remove navigation prompt
window.onbeforeunload = null

Let’s add it to our existing component

import React from 'react'
import PropTypes from 'prop-types'
import { Prompt } from 'react-router-dom'

const NavigationBlocker = props => {
  if (props.navigationBlocked) {
    window.onbeforeunload = () => true
  } else {
    window.onbeforeunload = null
  }
  return <Prompt when={props.navigationBlocked} message="Are you sure you want to leave?" />
}

NavigationBlocker.propTypes = {
  navigationBlocked: PropTypes.bool.isRequired,
}

export default NavigationBlocker

And just like that we have a navigation blocking component that we can easily turn on and off.

Extending it with Redux

I would suggest extending it to be a Global Navigation Blocker from here, when a processing redux action is sent send another to block navigation, when it resolves send another to resolve.

This allows you to include the blocker once at app level rather than including every time you need it.

This is what my implementation ended up looking like:

import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Prompt } from 'react-router-dom'

import { getNavigationBlocked } from '../navigation-selectors'

const GlobalNavigationBlocker = props => {
  if (props.navigationBlocked) {
    window.onbeforeunload = () => true
  } else {
    window.onbeforeunload = null
  }
  return <Prompt when={props.navigationBlocked} message="Are you sure you want to leave?" />
}

GlobalNavigationBlocker.propTypes = {
  navigationBlocked: PropTypes.bool.isRequired,
}

export const mapStateToProps = state => ({
  navigationBlocked: getNavigationBlocked(state),
})

export default connect(mapStateToProps)(GlobalNavigationBlocker)

Drawbacks

Unfortunately because of how onbeforeunload can be misused, modern browsers have implemented some safeguards, as described in the docs:

To combat unwanted pop-ups, some browsers don't display prompts created in beforeunload event handlers unless the page has been interacted with; some don't display them at all.

And

Various browsers ignore the result of the event and do not ask the user for confirmation at all … Firefox has a switch named dom.disable_beforeunload in about:config to enable this behaviour. (For eg.)

Or, TL;DR:

  • The user must interact with the page or the onbeforeunload event will not fire
  • onbeforeunload can be turned off with browser settings

You, also, cannot specify what message is shown to the user during this interaction.

All in all, I hope this saved you some time researching!