@allpro/react-router-pause

    1.1.3 • Public • Published

    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:

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

    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

    Keywords

    none

    Install

    npm i @allpro/react-router-pause

    DownloadsWeekly Downloads

    698

    Version

    1.1.3

    License

    MIT

    Unpacked Size

    982 kB

    Total Files

    10

    Last publish

    Collaborators

    • allpro