UNPKG

13 kBMarkdownView Raw
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.
14It provides a simple way to _asynchronously_ delay (pause)
15router navigation events triggered by the user actions.
16For example, if a user clicks a link while in the middle of a process,
17and 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)
24option,
25- and the
26[createHistory.getUserConfirmation()](https://github.com/ReactTraining/history/blob/master/README.md#customizing-the-confirm-dialog)
27option.
28
29**Motivation**
30
31The standard React Router
32[Prompt component](https://reacttraining.com/react-router/web/api/Prompt)
33is synchronous by default, so can display ONLY very basic `prompt()` messages.
34The same applies when using
35[router.history.block](https://github.com/ReactTraining/history#blocking-transitions).
36
37Browser `prompt()` dialogs are relatively **ugly** and cannot be customized.
38They are inconsistent with the attractive dialogs most modern apps use.
39**The motivation for RRP was it overcome this limitation.**
40
41It 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).
43However 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
65RRP is a React component, but does NOT render any output.
66RRP also does NOT display any prompts itself.
67It only provides a way for your code to hook into, and control the router.
68
69The RRP component accepts only two props (`when` is optional):
70
71```javascript
72<ReactRouterPause
73 use={handleNavigationAttempt}
74 when={isFormDirty}
75/>
76```
77
78A handler function could looks something like this:
79
80```javascript
81function 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
96The 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
112The function set in the `use` prop _handles_ navigation events.
113It is called _each time_ the router is about to change the location (URL).
114Three 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
135The `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.
148This means `navigation.resume()` can only trigger the **_last location_**
149clicked by the user.
150However, calling `navigation.cancel()` does make `navigation.isPaused()` more useful.
151
152#### Handler Return Values
153
154When called, the handler must return one of 5 values, (synchronously),
155back 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
163This example pauses navigation, then resumes after 10 seconds.
164
165```javascript
166function handleNavigationAttempt( navigation, location, action ) {
167 setTimeout( navigation.resume, 10000 ) // RESUME after 10 seconds
168 return null // null means PAUSE navigation
169}
170````
171
172This example returns a promise. Navigation is paused while validating
173data asynchronously. A message is displayed during this time.
174When the promise resolves, navigation will resume automatically.
175If the promise is rejected, the navigation event is cancelled.
176
177```javascript
178function 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
196A common use is to confirm that a user wishes to 'abort' a process, such as
197filling out a form. RRP allows a custom dialog (asynchronous) to be used.
198
199**Below are 2 example implementations using a 'confirmation dialog'.**
200The dialog is not important - it's just a _sample_ of how RRP can be used.
201
202### Functional Component Example
203
204This example keeps all code _inside_ the handler function,
205where it has access to the `navigation` methods.
206The [`setState` hook](https://reactjs.org/docs/hooks-state.html)
207is used to show and pass handlers to a confirmation dialog, (asynchronously).
208
209```javascript
210import React, { Fragment } from 'react'
211import { useFormManager } from '@allpro/form-manager'
212import ReactRouterPause from '@allpro/react-router-pause'
213
214import MyCustomDialog from './MyCustomDialog'
215
216// Functional Component using setState Hook
217function 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
256Here the navigation object is assigned as a class property so it is accessible
257to all other methods of the class.
258An alternative would be to _pass_ the navigation object to subroutines.
259
260```javascript
261import React, { Fragment } from 'react'
262import FormManager from '@allpro/form-manager'
263import ReactRouterPause from '@allpro/react-router-pause'
264
265import MyCustomDialog from './MyCustomDialog'
266
267// Functional Component using setState Hook
268class 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
335If you pull the repo, you can run a demo with `npm start`.
336
337I'll also put the demo on CodeSandbox soon.
338<br>Check back to get the URL here!
339
340## Contributing
341
342Please read
343[CONTRIBUTING.md](https://github.com/allpro/react-router-pause/blob/master/CONTRIBUTING.md)
344for details on our code of conduct,
345and the process for submitting pull requests to us.
346
347## Versioning
348
349We use SemVer for versioning. For the versions available,
350see the tags on this repository.
351
352## License
353
354This 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