1 | # React Router Pause (Async)
|
2 |
|
3 | [![npm package][npm-badge]][npm]
|
4 | [![gzip-size][gzip-size-badge]][gzip-size]
|
5 | [![install-size][install-size-badge]][install-size]
|
6 | [![build][build-badge]][build]
|
7 | [![coverage][coveralls-badge]][coveralls]
|
8 | [![license][license-badge]][license]
|
9 | [![donate][donate-badge]][donate]
|
10 |
|
11 |
|
12 | [React-Router-Pause](https://www.npmjs.com/package/@allpro/react-router-pause)
|
13 | (**"RRP"**) is a Javascript utility for React Router v4 & v5.
|
14 | It provides a simple way to _asynchronously_ delay (pause)
|
15 | router navigation events triggered by the user actions.
|
16 | For example, if a user clicks a link while in the middle of a process,
|
17 | and they will _lose data_ if navigation continues.
|
18 |
|
19 | **RRP is _similar to:_**
|
20 | - the React Router
|
21 | [Prompt](https://reacttraining.com/react-router/web/api/Prompt) component,
|
22 | - the
|
23 | [router.history.block](https://github.com/ReactTraining/history#blocking-transitions)
|
24 | option,
|
25 | - and the
|
26 | [createHistory.getUserConfirmation()](https://github.com/ReactTraining/history/blob/master/README.md#customizing-the-confirm-dialog)
|
27 | option.
|
28 |
|
29 | **Motivation**
|
30 |
|
31 | The standard React Router
|
32 | [Prompt component](https://reacttraining.com/react-router/web/api/Prompt)
|
33 | is synchronous by default, so can display ONLY very basic `prompt()` messages.
|
34 | The same applies when using
|
35 | [router.history.block](https://github.com/ReactTraining/history#blocking-transitions).
|
36 |
|
37 | Browser `prompt()` dialogs are relatively **ugly** and cannot be customized.
|
38 | They are inconsistent with the attractive dialogs most modern apps use.
|
39 | **The motivation for RRP was it overcome this limitation.**
|
40 |
|
41 | It is _possible_ to have an asychronous dialog by customizing
|
42 | [createHistory.getUserConfirmation()](https://github.com/ReactTraining/history/blob/master/README.md#customizing-the-confirm-dialog).
|
43 | However this is clumsy and allows only a single, global configuration.
|
44 |
|
45 | **Advantages of RRP**
|
46 |
|
47 | - Useful for anything async; not just 'prompt messages'.
|
48 | - _Very easy_ to add asynchronous navigation blocking.
|
49 | - Fully customizable by each component - _no limitations_.
|
50 | - Does not require modifying the history object.
|
51 | - Is compatible with React Native and server-side-rendering.
|
52 |
|
53 |
|
54 | ## Installation
|
55 |
|
56 | - NPM: `npm install @allpro/react-router-pause`
|
57 | - Yarn: `yarn add @allpro/react-router-pause`
|
58 | - CDN: Exposed global is `ReactRouterPause`
|
59 | - Unpkg: `<script src="https://unpkg.com/@allpro/react-router-pause/umd/@allpro/react-router-pause.min.js"></script>`
|
60 | - JSDelivr: `<script src="https://cdn.jsdelivr.net/npm/@allpro/react-router-pause/umd/@allpro/react-router-pause.min.js"></script>`
|
61 |
|
62 |
|
63 | ## Usage
|
64 |
|
65 | RRP is a React component, but does NOT render any output.
|
66 | RRP also does NOT display any prompts itself.
|
67 | It only provides a way for your code to hook into, and control the router.
|
68 |
|
69 | The RRP component accepts only two props (`when` is optional):
|
70 |
|
71 | ```javascript
|
72 | <ReactRouterPause
|
73 | use={handleNavigationAttempt}
|
74 | when={isFormDirty}
|
75 | />
|
76 | ```
|
77 |
|
78 | A handler function could looks something like this:
|
79 |
|
80 | ```javascript
|
81 | function handleNavigationAttempt( navigation, location, action ) {
|
82 | // Call an async function that returns a promise; wait for it to resolve...
|
83 | preloadNextPage( location )
|
84 | .then( navigation.resume ) // CONTINUE with navigation event
|
85 | .catch(error => {
|
86 | navigation.cancel() // CLEAR the navigation data (optional)
|
87 | displayErrorMessage(error)
|
88 | })
|
89 |
|
90 | return null // Returning null means PAUSE navigation
|
91 | }
|
92 | ````
|
93 |
|
94 | ### Properties
|
95 |
|
96 | The RRP component accepts two props:
|
97 |
|
98 | - #### `use` `{function}` `[null]` _`required`_
|
99 |
|
100 | Called each time a router navigation event occurs.
|
101 | See 'Event Handler' details below.
|
102 |
|
103 | - #### `when` `{boolean}` `[true]` _`optional`_
|
104 |
|
105 | Set `when={false}` to disable the RRP component.
|
106 | This is an alternative to using conditional rendering.
|
107 | <br>_(Works same as Prompt component `when` prop.)_
|
108 |
|
109 |
|
110 | ### Event Handler
|
111 |
|
112 | The function set in the `use` prop _handles_ navigation events.
|
113 | It is called _each time_ the router is about to change the location (URL).
|
114 | Three arguments are passed when the handler is called:
|
115 |
|
116 | - #### `navigation` `{object}`
|
117 |
|
118 | The methods in this object provide control of the RRP component.
|
119 | See 'RRP Object/Methods' below for details.
|
120 |
|
121 | - #### `location` `{object}`
|
122 |
|
123 | A React Router
|
124 | [`location`](https://reacttraining.com/react-router/web/api/location)
|
125 | object describing the navigation triggered by the user.
|
126 |
|
127 | - #### `action` `{string}`
|
128 |
|
129 | The action that triggered the navigation.
|
130 | <br>One of `PUSH`, `REPLACE`, or `POP`
|
131 |
|
132 |
|
133 | #### `navigation` Object/Methods
|
134 |
|
135 | The `navigation` object passed to the handler function provides 5 methods:
|
136 |
|
137 | - **navigation.isPaused()** - Returns `true` or `false` to indicate if any navigation
|
138 | event is currently paused.
|
139 | - **navigation.resume()** - Triggers the 'paused' navigation event to run
|
140 | - **navigation.cancel()** - Clears 'paused' navigation so can no longer be resumed.
|
141 | - **navigation.push(** path, state **)** - The `router.history.push()` method,
|
142 | in case you wish to redirect a user to an alternate location
|
143 | - **navigation.replace(** path, state **)** - The `router.history.replace()` method,
|
144 | in case you wish to redirect a user to an alternate location
|
145 |
|
146 | **NOTE: It is not _necessary_ to call `navigation.clear()`.**
|
147 | <br>Each new navigation event will _replace_ the previous one.
|
148 | This means `navigation.resume()` can only trigger the **_last location_**
|
149 | clicked by the user.
|
150 | However, calling `navigation.cancel()` does make `navigation.isPaused()` more useful.
|
151 |
|
152 | #### Handler Return Values
|
153 |
|
154 | When called, the handler must return one of 5 values, (synchronously),
|
155 | back to the RRP component. These are:
|
156 |
|
157 | - **`true`** or **`undefined`** - Allow navigation to continue.
|
158 | - **`false`** - Cancel the navigation event, permanently.
|
159 | - **`null`** - Pause navigation so can _optionally_ be resumed later.
|
160 | - **`Promise`** - Pause until promise is settled,
|
161 | then resume if promise resolves, or cancel if rejected.
|
162 |
|
163 | This example pauses navigation, then resumes after 10 seconds.
|
164 |
|
165 | ```javascript
|
166 | function handleNavigationAttempt( navigation, location, action ) {
|
167 | setTimeout( navigation.resume, 10000 ) // RESUME after 10 seconds
|
168 | return null // null means PAUSE navigation
|
169 | }
|
170 | ````
|
171 |
|
172 | This example returns a promise. Navigation is paused while validating
|
173 | data asynchronously. A message is displayed during this time.
|
174 | When the promise resolves, navigation will resume automatically.
|
175 | If the promise is rejected, the navigation event is cancelled.
|
176 |
|
177 | ```javascript
|
178 | function handleNavigationAttempt( navigation, location, action ) {
|
179 | displayProcessingMessage()
|
180 |
|
181 | return verifySomething(data)
|
182 | .then(isValid => {
|
183 | if (!isValid) {
|
184 | hideProcessingMessage()
|
185 | showErrorMessage()
|
186 | return Promise.reject() // Cancel Navigation Event
|
187 | }
|
188 | // If not rejected, navigation will now Resume
|
189 | })
|
190 | }
|
191 | ````
|
192 |
|
193 |
|
194 | ## Implementation
|
195 |
|
196 | A common use is to confirm that a user wishes to 'abort' a process, such as
|
197 | filling out a form. RRP allows a custom dialog (asynchronous) to be used.
|
198 |
|
199 | **Below are 2 example implementations using a 'confirmation dialog'.**
|
200 | The dialog is not important - it's just a _sample_ of how RRP can be used.
|
201 |
|
202 | ### Functional Component Example
|
203 |
|
204 | This example keeps all code _inside_ the handler function,
|
205 | where it has access to the `navigation` methods.
|
206 | The [`setState` hook](https://reactjs.org/docs/hooks-state.html)
|
207 | is used to show and pass handlers to a confirmation dialog, (asynchronously).
|
208 |
|
209 | ```javascript
|
210 | import React, { Fragment } from 'react'
|
211 | import { useFormManager } from '@allpro/form-manager'
|
212 | import ReactRouterPause from '@allpro/react-router-pause'
|
213 |
|
214 | import MyCustomDialog from './MyCustomDialog'
|
215 |
|
216 | // Functional Component using setState Hook
|
217 | function myFormComponent( props ) {
|
218 | // Sample form handler so can check form.isDirty()
|
219 | const form = useFormManager( formConfig, props.data )
|
220 |
|
221 | const [ dialogProps, setDialogProps ] = useState({ open: false })
|
222 | const closeDialog = () => setDialogProps({ open: false })
|
223 |
|
224 | function handleNavigationAttempt( navigation, location, action ) {
|
225 | setDialogProps({
|
226 | open: true,
|
227 | handleStay: () => { closeDialog(); navigation.cancel() },
|
228 | handleLeave: () => { closeDialog(); navigation.resume() },
|
229 | handleHelp: () => { closeDialog(); navigation.push('/form-help') }
|
230 | })
|
231 |
|
232 | // Return null to 'pause' and save the route so can 'resume'
|
233 | return null
|
234 | }
|
235 |
|
236 | return (
|
237 | <Fragment>
|
238 | <ReactRouterPause
|
239 | use={handleNavigationAttempt}
|
240 | when={form.isDirty()}
|
241 | />
|
242 |
|
243 | <MyCustomDialog {...dialogProps}>
|
244 | If you leave this page, your data will be lost.
|
245 | Are you sure you want to leave?
|
246 | </MyCustomDialog>
|
247 |
|
248 | ...
|
249 | </Fragment>
|
250 | )
|
251 | }
|
252 | ```
|
253 |
|
254 | ### Class Component Example
|
255 |
|
256 | Here the navigation object is assigned as a class property so it is accessible
|
257 | to all other methods of the class.
|
258 | An alternative would be to _pass_ the navigation object to subroutines.
|
259 |
|
260 | ```javascript
|
261 | import React, { Fragment } from 'react'
|
262 | import FormManager from '@allpro/form-manager'
|
263 | import ReactRouterPause from '@allpro/react-router-pause'
|
264 |
|
265 | import MyCustomDialog from './MyCustomDialog'
|
266 |
|
267 | // Functional Component using setState Hook
|
268 | class myFormComponent extends React.Component {
|
269 | constructor(props) {
|
270 | super(props)
|
271 | this.form = FormManager(this, formConfig, props.data)
|
272 | this.state = { showDialog: false }
|
273 | this.navigation = null
|
274 | }
|
275 |
|
276 | handleNavigationAttempt( navigation, location, action ) {
|
277 | this.navigation = navigation
|
278 | this.setState({ showDialog: true })
|
279 |
|
280 | // Return null to 'pause' and save the route so can 'resume'
|
281 | return null
|
282 | }
|
283 |
|
284 | closeDialog() {
|
285 | this.setState({ showDialog: false })
|
286 | }
|
287 |
|
288 | handleStay() {
|
289 | // NOTE: It's not necessary to 'cancel' paused navigation
|
290 | // Deletes the cached navigation data so can no longer be resumed
|
291 | this.navigation.cancel()
|
292 | this.closeDialog()
|
293 | }
|
294 |
|
295 | handleLeave() {
|
296 | // Navigate to whatever destination the user clicked
|
297 | this.navigation.resume()
|
298 | this.closeDialog()
|
299 | }
|
300 |
|
301 | handleShowHelp() {
|
302 | // NOTE: It's not necessary to 'cancel' paused navigation
|
303 | this.navigation.push('/form-help')
|
304 | this.closeDialog()
|
305 | }
|
306 |
|
307 | render() {
|
308 | return (
|
309 | <Fragment>
|
310 | <ReactRouterPause
|
311 | use={this.handleNavigationAttempt}
|
312 | when={this.form.isDirty()}
|
313 | />
|
314 |
|
315 | {this.state.showDialog &&
|
316 | <MyCustomDialog
|
317 | onClickStay={this.handleStay}
|
318 | onClickLeave={this.handleLeave}
|
319 | onClickHelp={this.handleShowHelp}
|
320 | >
|
321 | If you leave this page, your data will be lost.
|
322 | Are you sure you want to leave?
|
323 | </MyCustomDialog>
|
324 | }
|
325 | ...
|
326 | </Fragment>
|
327 | )
|
328 | }
|
329 | }
|
330 | ```
|
331 |
|
332 |
|
333 | ## Live Demo
|
334 |
|
335 | If you pull the repo, you can run a demo with `npm start`.
|
336 |
|
337 | I'll also put the demo on CodeSandbox soon.
|
338 | <br>Check back to get the URL here!
|
339 |
|
340 | ## Contributing
|
341 |
|
342 | Please read
|
343 | [CONTRIBUTING.md](https://github.com/allpro/react-router-pause/blob/master/CONTRIBUTING.md)
|
344 | for details on our code of conduct,
|
345 | and the process for submitting pull requests to us.
|
346 |
|
347 | ## Versioning
|
348 |
|
349 | We use SemVer for versioning. For the versions available,
|
350 | see the tags on this repository.
|
351 |
|
352 | ## License
|
353 |
|
354 | This project is licensed under the MIT License - see the
|
355 | [LICENSE.md](https://github.com/allpro/react-router-pause/blob/master/LICENSE)
|
356 | file for details
|
357 |
|
358 |
|
359 | [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
|
360 | [gzip-size]: http://img.badgesize.io/https://cdn.jsdelivr.net/npm/@allpro/react-router-pause/umd/@allpro/react-router-pause.min.js
|
361 |
|
362 | [install-size-badge]: https://packagephobia.now.sh/badge?p=@allpro/react-router-pause
|
363 | [install-size]: https://packagephobia.now.sh/result?p=@allpro/react-router-pause
|
364 |
|
365 | [npm-badge]: http://img.shields.io/npm/v/@allpro/react-router-pause.svg?style=flat-round
|
366 | [npm]: https://www.npmjs.com/package/@allpro/react-router-pause
|
367 |
|
368 | [build-badge]: https://travis-ci.org/allpro/react-router-pause.svg?branch=master
|
369 | [build]: https://travis-ci.org/allpro/react-router-pause
|
370 |
|
371 | [coveralls-badge]: https://coveralls.io/repos/github/allpro/react-router-pause/badge.svg?branch=master
|
372 | [coveralls]: https://coveralls.io/github/allpro/react-router-pause?branch=master
|
373 |
|
374 | [license-badge]: https://badgen.now.sh/badge/license/MIT/blue
|
375 | [license]: https://github.com/allpro/form-manager/blob/master/LICENSE
|
376 |
|
377 | [donate-badge]: https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat-round
|
378 | [donate]: https://paypal.me/KevinDalman
|