1
1
/* eslint-disable react/prefer-stateless-function */
2
2
3
- import React , { useState , useEffect , useRef } from 'react'
3
+ import React , { useState , useEffect , useRef , useCallback } from 'react'
4
4
import { Meteor } from 'meteor/meteor'
5
5
import { Mongo } from 'meteor/mongo'
6
6
import { Tracker } from 'meteor/tracker'
@@ -13,6 +13,8 @@ const globalTrackerQueue: Array<Function> = []
13
13
let globalTrackerTimestamp : number | undefined = undefined
14
14
let globalTrackerTimeout : number | undefined = undefined
15
15
16
+ const SUBSCRIPTION_TIMEOUT = 1000
17
+
16
18
/**
17
19
* Delay an update to be batched with the global tracker invalidation queue
18
20
*/
@@ -370,6 +372,61 @@ export function useTracker<T, K extends undefined | T = undefined>(
370
372
return meteorData
371
373
}
372
374
375
+ /**
376
+ * A hook to track a boolean state with a sort of histeresis, with preference for `true`. `setState` makes the returned
377
+ * `state` be `true` immediately, but `false` only after `resetState` is called and `timeout` elapses. If `setState`
378
+ * is called with `true` before `timeout` elapses, then `resetState` is aborted and `state` will remain `ture.
379
+ *
380
+ * Later `resetState` calls replace earlier unelapsed calls and their timeout periods.
381
+ *
382
+ * @param {boolean } [initialState=false]
383
+ * @return {* } {{
384
+ * state: boolean
385
+ * setState: (value: boolean) => void
386
+ * resetState: (timeout: number) => void
387
+ * }}
388
+ */
389
+ function useDelayState ( initialState = false ) : {
390
+ state : boolean
391
+ setState : ( value : boolean ) => void
392
+ resetState : ( timeout : number ) => void
393
+ } {
394
+ const [ state , setState ] = useState ( initialState )
395
+ const [ prevState , setPrevState ] = useState ( initialState )
396
+ const prevReadyTimeoutRef = useRef < number | null > ( null )
397
+
398
+ const setStateAndClearResets = useCallback (
399
+ ( value : boolean ) => {
400
+ setState ( value )
401
+
402
+ if ( value ) {
403
+ setPrevState ( true )
404
+ if ( prevReadyTimeoutRef . current !== null ) {
405
+ window . clearTimeout ( prevReadyTimeoutRef . current )
406
+ prevReadyTimeoutRef . current = null
407
+ }
408
+ }
409
+ } ,
410
+ [ setState , setPrevState ]
411
+ )
412
+
413
+ const resetStateAfterDelay = useCallback ( ( timeout : number ) => {
414
+ if ( prevReadyTimeoutRef . current !== null ) {
415
+ window . clearTimeout ( prevReadyTimeoutRef . current )
416
+ }
417
+ prevReadyTimeoutRef . current = window . setTimeout ( ( ) => {
418
+ prevReadyTimeoutRef . current = null
419
+ setPrevState ( false )
420
+ } , timeout )
421
+ } , [ ] )
422
+
423
+ return {
424
+ state : state || prevState ,
425
+ setState : setStateAndClearResets ,
426
+ resetState : resetStateAfterDelay ,
427
+ }
428
+ }
429
+
373
430
/**
374
431
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
375
432
* Subscriptions will be torn down 1000ms after unmounting the component.
@@ -383,20 +440,28 @@ export function useSubscription<K extends keyof AllPubSubTypes>(
383
440
sub : K ,
384
441
...args : Parameters < AllPubSubTypes [ K ] >
385
442
) : boolean {
386
- const [ ready , setReady ] = useState < boolean > ( false )
443
+ const { state : ready , setState : setReady , resetState : cancelPreviousReady } = useDelayState ( )
387
444
388
445
useEffect ( ( ) => {
389
446
const subscription = Tracker . nonreactive ( ( ) => meteorSubscribe ( sub , ...args ) )
390
- const isReadyComp = Tracker . nonreactive ( ( ) => Tracker . autorun ( ( ) => setReady ( subscription . ready ( ) ) ) )
447
+ const isReadyComp = Tracker . nonreactive ( ( ) =>
448
+ Tracker . autorun ( ( ) => {
449
+ const isNowReady = subscription . ready ( )
450
+ setReady ( isNowReady )
451
+ } )
452
+ )
391
453
return ( ) => {
392
454
isReadyComp . stop ( )
393
455
setTimeout ( ( ) => {
394
456
subscription . stop ( )
395
- } , 1000 )
457
+ } , SUBSCRIPTION_TIMEOUT )
458
+ cancelPreviousReady ( SUBSCRIPTION_TIMEOUT )
396
459
}
397
460
} , [ sub , stringifyObjects ( args ) ] )
398
461
399
- return ready
462
+ const isReady = ready
463
+
464
+ return isReady
400
465
}
401
466
402
467
/**
@@ -415,7 +480,7 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
415
480
enable : boolean ,
416
481
...args : Parameters < AllPubSubTypes [ K ] >
417
482
) : boolean {
418
- const [ ready , setReady ] = useState < boolean > ( false )
483
+ const { state : ready , setState : setReady , resetState : cancelPreviousReady } = useDelayState ( )
419
484
420
485
useEffect ( ( ) => {
421
486
if ( ! enable ) {
@@ -424,16 +489,69 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
424
489
}
425
490
426
491
const subscription = Tracker . nonreactive ( ( ) => meteorSubscribe ( sub , ...args ) )
427
- const isReadyComp = Tracker . nonreactive ( ( ) => Tracker . autorun ( ( ) => setReady ( subscription . ready ( ) ) ) )
492
+ const isReadyComp = Tracker . nonreactive ( ( ) =>
493
+ Tracker . autorun ( ( ) => {
494
+ const isNowReady = subscription . ready ( )
495
+ setReady ( isNowReady )
496
+ } )
497
+ )
428
498
return ( ) => {
429
499
isReadyComp . stop ( )
430
500
setTimeout ( ( ) => {
431
501
subscription . stop ( )
432
- } , 1000 )
502
+ } , SUBSCRIPTION_TIMEOUT )
503
+ cancelPreviousReady ( SUBSCRIPTION_TIMEOUT )
433
504
}
434
505
} , [ sub , enable , stringifyObjects ( args ) ] )
435
506
436
- return ! enable || ready
507
+ const isReady = ! enable || ready
508
+
509
+ return isReady
510
+ }
511
+
512
+ /**
513
+ * A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
514
+ * Subscriptions will be torn down 1000ms after unmounting the component.
515
+ * If the subscription is not enabled, the subscription will not be created, and the ready state will always be true.
516
+ *
517
+ * @export
518
+ * @param {PubSub } sub The subscription to be subscribed to
519
+ * @param {boolean } enable Whether the subscription is enabled
520
+ * @param {...any[] } args A list of arugments for the subscription. This is used for optimizing the subscription across
521
+ * renders so that it isn't torn down and created for every render.
522
+ */
523
+ export function useSubscriptionIfEnabledReadyOnce < K extends keyof AllPubSubTypes > (
524
+ sub : K ,
525
+ enable : boolean ,
526
+ ...args : Parameters < AllPubSubTypes [ K ] >
527
+ ) : boolean {
528
+ const { state : ready , setState : setReady , resetState : cancelPreviousReady } = useDelayState ( )
529
+
530
+ useEffect ( ( ) => {
531
+ if ( ! enable ) {
532
+ setReady ( false )
533
+ return
534
+ }
535
+
536
+ const subscription = Tracker . nonreactive ( ( ) => meteorSubscribe ( sub , ...args ) )
537
+ const isReadyComp = Tracker . nonreactive ( ( ) =>
538
+ Tracker . autorun ( ( ) => {
539
+ const isNowReady = subscription . ready ( )
540
+ if ( isNowReady ) setReady ( true )
541
+ } )
542
+ )
543
+ return ( ) => {
544
+ isReadyComp . stop ( )
545
+ setTimeout ( ( ) => {
546
+ subscription . stop ( )
547
+ } , SUBSCRIPTION_TIMEOUT )
548
+ cancelPreviousReady ( SUBSCRIPTION_TIMEOUT )
549
+ }
550
+ } , [ sub , enable , stringifyObjects ( args ) ] )
551
+
552
+ const isReady = ! enable || ready
553
+
554
+ return isReady
437
555
}
438
556
439
557
/**
0 commit comments