Skip to content

Latest commit

 

History

History
440 lines (329 loc) · 14.7 KB

README.md

File metadata and controls

440 lines (329 loc) · 14.7 KB

React Router Pause (Async)

npm package gzip-size install-size build coverage license 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:

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.

Live Example

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

Installation

  • 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>

Compatibility

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.

"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.

Usage

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.

Component Properties

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.
    See handler Function below.

  • when   {boolean} [true]   optional
    Set when={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?
      If false, bookmark-links are treated the same as page-links.
Example
<ReactRouterPause 
    handler={ handleNavigationAttempt }
    when={ isFormDirty }
    config={{ allowBookmarks: false }}
/>

handler Function

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 event is currently paused.

  • navigation.pausedLocation()
    Returns the location object representing the paused navigation, or null if no event is paused.

  • navigation.pause()
    Pause navigation event - equivalent to returning null 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 return false.
    NOTE: It is usually not necessary to call navigation.cancel().

  • navigation.push(path, state)
    The router.history.push() method; allows redirecting a user to an alternate location.

  • navigation.replace(path, state)
    The router.history.replace() 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.

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'
	    })
}

Same-Location Blocking

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:

The location 'hash' (bookmark) is ignored by default.
See config.allowBookmarks in the Component Properties section.

Implementation

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.

Functional Component Example

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>
    )
}

Class Component Example

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>
        )
    }
}

Built With

Contributing

Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

License

MIT © allpro
See LICENSE file for details