React-Router-Pause ("RRP") is a Javascript utility for React Router v4 & v5. It provides a simple way to asynchronously delay (pause) router navigation events triggered by the user. For example, if a user clicks a link while in the middle of a process, and they will lose data if navigation continues.
For more detail, see: Control React Router, Asynchronously
RRP is similar to:
- the React Router Prompt component,
- the router.history.block option,
- and the createHistory.getUserConfirmation() option.
Motivation
The standard React Router
Prompt component
is synchronous by default, so can display ONLY window.prompt()
messages. The same applies when using
router.history.block.
The window.prompt()
dialog is relatively ugly and cannot be
customized. They are inconsistent with the attractive dialogs most modern
apps use. The motivation for RRP was it overcome this limitation.
It is possible to have an asychronous dialog by customizing createHistory.getUserConfirmation(). However this is clumsy and allows only a single, global configuration.
Advantages of RRP
- Useful for anything async; not just 'prompt messages'.
- Very easy to add asynchronous navigation blocking.
- Fully customizable by each component - no limitations.
- Does not require modifying the history object.
- Is compatible with React Native and server-side-rendering.
Try the demo at: https://allpro.github.io/react-router-pause
Play with the demo code at: https://codesandbox.io/s/github/allpro/react-router-pause/tree/master/example
If you pull or fork the repo, you can run the demo like this:
- In the root folder, run
npm start
- In a second terminal, in the
/example
folder, runnpm start
- The demo will start at http://localhost:3000
- Changes to the component or the demo will auto-update the browser
- NPM:
npm install @allpro/react-router-pause
- Yarn:
yarn add @allpro/react-router-pause
- CDN: Exposed global is
ReactRouterPause
- Unpkg:
<script src="https://unpkg.com/@allpro/react-router-pause/umd/react-router-pause.min.js"></script>
- JSDelivr:
<script src="https://cdn.jsdelivr.net/npm/@allpro/react-router-pause/umd/react-router-pause.min.js"></script>
- Unpkg:
RRP is designed for maximum backwards compatibility.
It's a React class-component that utilizes the withRouter()
HOC provided
by React-Router 4+.
RRP does not hack the router context or use any non-standard trickery
that might cause compatibility issues in the future.
RRP will work in any project using React-Router 4.x or 5.x, which requires React >=15.
"peerDependencies": {
"prop-types": ">=15",
"react": ">=15",
"react-dom": ">=15",
"react-router-dom": ">=4"
}
There is also a version of RRP using React-hooks.
This is not exported because it requires React 16.8 or higher,
so is not compatible with older projects.
This version is in the repo for anyone interested:
https://github.com/allpro/react-router-pause/blob/master/src/ReactRouterPauseHooks.js
When React-Router is eventually updated to provide React-hooks, the RRP hooks-version will be updated to take advantage of this. It may become the recommended version for projects using the updated React-Router.
RRP is a React component, but does NOT render any output. RRP also does NOT display any prompts itself. It only provides a way for your code to hook into and control the router.
The RRP component accepts 3 props:
-
handler
{function}[null]
optional
This is called each time a navigation event occurs.
If a handler is not provided, RRP is disabled.
Seehandler
Function below. -
when
{boolean}[true]
optional
Setwhen={false}
to temporarily disable the RRP component. This is an alternative to using conditional rendering. -
config
{object}[{}]
optional
A configuration object to change RRP logic.config.allowBookmarks
{boolean}[true]
Should bookmark-links for same page always be allowed?
Iffalse
, bookmark-links are treated the same as page-links.
<ReactRouterPause
handler={ handleNavigationAttempt }
when={ isFormDirty }
config={{ allowBookmarks: false }}
/>
The function set in props.handler
will be called before the router
changes the location (URL).
Three arguments are passed to the handler
:
-
navigation
{object}
An API that provides control of the navigation.
Seenavigation
API Methods" below. -
location
{object}
A React Routerlocation
object that describes the navigation event. -
action
{string}
The event-action type:PUSH
,REPLACE
, orPOP
The navigation
API passed to the handler has these methods:
-
navigation.isPaused()
Returnstrue
orfalse
to indicate if a navigation event is currently paused. -
navigation.pausedLocation()
Returns thelocation
object representing the paused navigation, ornull
if no event is paused. -
navigation.pause()
Pause navigation event - equivalent to returningnull
from the handler.
Note: This must be called before the handler returns. -
navigation.resume()
Triggers the 'paused' navigation event to occur. -
navigation.cancel()
-
Clears 'paused' navigation so it can no longer be resumed.
After cancelling,navigation.isPaused()
will returnfalse
.
NOTE: It is usually not necessary to callnavigation.cancel()
. -
navigation.push(
path, state
)
Therouter.history.push()
method; allows redirecting a user to an alternate location. -
navigation.replace(
path, state
)
Therouter.history.replace()
method; allows redirecting a user to an alternate location.
If the handler does NOT call any navigationAPI method is before it returns, then it must return one of these responses:
true
orundefined
- Allow navigation to continue.false
- Cancel the navigation event, permanently.null
- Pause navigation so can optionally be resumed later.Promise
- Pause navigation until promise is settled, then:- If promise is rejected, cancel navigation
- If promise resolves with a value of
false
, cancel navigation - If promise resolves with any other value, resume navigation
This example pauses navigation, then resumes after 10 seconds.
function handleNavigationAttempt( navigation, location, action ) {
setTimeout( navigation.resume, 10000 ) // RESUME after 10 seconds
return null // null means PAUSE navigation
}
The example below returns a promise to pause navigation while validating
data asynchronously. If the promise resolves,
navigation will resume unless false
is returned by promise.
If the promise rejects, navigation is cancelled.
function handleNavigationAttempt( navigation, location, action ) {
return verifySomething(data)
.then(isValid => {
if (!isValid) {
showErrorMessage()
return false // Cancel Navigation
}
// Navigation resumes if 'false' not returned, and not 'rejected'
})
}
RRP automatically blocks navigation if the new location is the same as the current location. This prevents scenarios where React Router reloads a form when the user clicks the same page-link again.
The comparison between two locations includes:
- pathname ("https://domain.com/section/page.html")
- search ("?key=value&otherValues")
- state ("value" or { foo: 'bar' })
The location 'hash' (bookmark) is ignored by default.
See config.allowBookmarks
in the
Component Properties section.
A common requirement in an app is to ask a user if they wants to 'abort' a process, (such as filling out a form), when they click a navigation link.
Below are 2 examples using a custom 'confirmation dialog', showing different ways to integrate RRP with your code.
This example keeps all code inside the handler function,
where it has access to the navigation
methods.
The setState
hook
is used to store and pass handlers to a confirmation dialog.
import React, { Fragment } from 'react'
import { useFormManager } from '@allpro/form-manager'
import ReactRouterPause from '@allpro/react-router-pause'
import MyCustomDialog from './MyCustomDialog'
// Functional Component using setState Hook
function myFormComponent( props ) {
// Sample form handler so can check form.isDirty()
const form = useFormManager( formConfig, props.data )
const [ dialogProps, setDialogProps ] = useState({ open: false })
const closeDialog = () => setDialogProps({ open: false })
function handleNavigationAttempt( navigation, location, action ) {
setDialogProps({
open: true,
handleStay: () => { closeDialog(); navigation.cancel() },
handleLeave: () => { closeDialog(); navigation.resume() },
handleHelp: () => { closeDialog(); navigation.push('/form-help') }
})
// Return null to 'pause' and save the route so can 'resume'
return null
}
return (
<Fragment>
<ReactRouterPause
handler={handleNavigationAttempt}
when={form.isDirty()}
/>
<MyCustomDialog {...dialogProps}>
If you leave this page, your data will be lost.
Are you sure you want to leave?
</MyCustomDialog>
...
</Fragment>
)
}
In this example, the navigation API object is assigned to a property so it is accessible to every method in the class.
import React, { Fragment } from 'react'
import FormManager from '@allpro/form-manager'
import ReactRouterPause from '@allpro/react-router-pause'
import MyCustomDialog from './MyCustomDialog'
// Functional Component using setState Hook
class myFormComponent extends React.Component {
constructor(props) {
super(props)
this.form = FormManager(this, formConfig, props.data)
this.state = { showDialog: false }
this.navigation = null
}
handleNavigationAttempt( navigation, location, action ) {
this.navigation = navigation
this.setState({ showDialog: true })
// Return null to 'pause' and save the route so can 'resume'
return null
}
closeDialog() {
this.setState({ showDialog: false })
}
handleStay() {
this.closeDialog()
this.navigation.cancel()
}
handleLeave() {
this.closeDialog()
this.navigation.resume()
}
handleShowHelp() {
this.closeDialog()
this.navigation.push('/form-help')
}
render() {
return (
<Fragment>
<ReactRouterPause
handler={this.handleNavigationAttempt}
when={this.form.isDirty()}
/>
{this.state.showDialog &&
<MyCustomDialog
onClickStay={this.handleStay}
onClickLeave={this.handleLeave}
onClickHelp={this.handleShowHelp}
>
If you leave this page, your data will be lost.
Are you sure you want to leave?
</MyCustomDialog>
}
...
</Fragment>
)
}
}
- create-react-library - A React component framework based on create-react-app
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
We use SemVer for versioning. For the versions available, see the tags on this repository.