Helper for React Router to provide customizable asynchronous navigation blocking
npm install @allpro/react-router-pause[![npm package][npm-badge]][npm]
[![gzip-size][gzip-size-badge]][gzip-size]
[![install-size][install-size-badge]][install-size]
[![build][build-badge]][build]
[![coverage][coveralls-badge]][coveralls]
[![license][license-badge]][license]
[![donate][donate-badge]][donate]
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, run npm 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:
- JSDelivr:
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.
#### Peer-Dependencies
RRP will work in _any project_ using React-Router 4.x _or_ 5.x,
which requires React >=15.
``json`
"peerDependencies": {
"prop-types": ">=15",
"react": ">=15",
"react-dom": ">=15",
"react-router-dom": ">=4"
}
#### React-Hooks Testing Version
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_handler
This is called _each time_ a navigation event occurs.
If a handler is not provided, RRP is disabled.
See Function below.
- when {boolean} [true] _optional_when={false}
Set 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]false
Should bookmark-links for same page _always_ be allowed?
If , bookmark-links are treated the same as page-links.
###### Example
`javascript`
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.
See navigation API Methods" below.
- location {object}
A React Router
location
object that describes the navigation event.
- action {string}
The event-action type:
PUSH, REPLACE, or POP
#### navigation API Methods
The navigation API passed to the handler has these methods:
- navigation.isPaused()
Returns true or false to indicate if a navigation.pausedLocation()
navigation event is currently paused.
- location
Returns the object representing the paused navigation, null
or if no event is paused.navigation.pause()
- null
Pause navigation event - equivalent to returning from the handler.navigation.resume()
Note: This must be called _before_ the handler returns.
- navigation.cancel()
Triggers the 'paused' navigation event to occur.
- - navigation.isPaused()
Clears 'paused' navigation so it can no longer be resumed.
After cancelling, will return false.navigation.cancel()
NOTE: It is _usually not necessary_ to call . navigation.push(
- path, state)router.history.push()
The method;navigation.replace(
allows redirecting a user to an alternate location.
- path, state)router.history.replace()
The method;
allows redirecting a user to an alternate location.
#### handler Function Return Values
If the handler does NOT call any navigationAPI method is before it returns,
then it must return one of these responses:
- true or undefined - 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.
`javascript``
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.
`javascript``
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. setState
The hook
is used to store and pass handlers to a confirmation dialog.
`javascript
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 (
when={form.isDirty()}
/>
If you leave this page, your data will be lost.
Are you sure you want to leave?
...
)
}
`
In this example, the navigation API object is assigned to a property
so it is accessible to every method in the class.
`javascript
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 (
when={this.form.isDirty()}
/>
{this.state.showDialog &&
onClickLeave={this.handleLeave}
onClickHelp={this.handleShowHelp}
>
If you leave this page, your data will be lost.
Are you sure you want to leave?
}
...
)
}
}
``
- 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.
MIT © allpro
See
LICENSE
file for details
[gzip-size-badge]: http://img.badgesize.io/https://cdn.jsdelivr.net/npm/@allpro/react-router-pause/umd/@allpro/react-router-pause.min.js?compression=gzip
[gzip-size]: http://img.badgesize.io/https://cdn.jsdelivr.net/npm/@allpro/react-router-pause/umd/@allpro/react-router-pause.min.js
[install-size-badge]: https://packagephobia.now.sh/badge?p=@allpro/react-router-pause
[install-size]: https://packagephobia.now.sh/result?p=@allpro/react-router-pause
[npm-badge]: http://img.shields.io/npm/v/@allpro/react-router-pause.svg?style=flat-round
[npm]: https://www.npmjs.com/package/@allpro/react-router-pause
[build-badge]: https://travis-ci.org/allpro/react-router-pause.svg?branch=master
[build]: https://travis-ci.org/allpro/react-router-pause
[coveralls-badge]: https://coveralls.io/repos/github/allpro/react-router-pause/badge.svg?branch=master
[coveralls]: https://coveralls.io/github/allpro/react-router-pause?branch=master
[license-badge]: https://badgen.now.sh/badge/license/MIT/blue
[license]: https://github.com/allpro/react-router-pause/blob/master/LICENSE
[donate-badge]: https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat-round
[donate]: https://paypal.me/KevinDalman