11/* eslint-disable react/prefer-stateless-function */
22
3- import React , { useState , useEffect , useRef } from 'react'
3+ import React , { useState , useEffect , useRef , useCallback } from 'react'
44import { Meteor } from 'meteor/meteor'
55import { Mongo } from 'meteor/mongo'
66import { Tracker } from 'meteor/tracker'
@@ -13,6 +13,8 @@ const globalTrackerQueue: Array<Function> = []
1313let globalTrackerTimestamp : number | undefined = undefined
1414let globalTrackerTimeout : number | undefined = undefined
1515
16+ const SUBSCRIPTION_TIMEOUT = 1000
17+
1618/**
1719 * Delay an update to be batched with the global tracker invalidation queue
1820 */
@@ -370,6 +372,61 @@ export function useTracker<T, K extends undefined | T = undefined>(
370372 return meteorData
371373}
372374
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+
373430/**
374431 * A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
375432 * Subscriptions will be torn down 1000ms after unmounting the component.
@@ -383,20 +440,28 @@ export function useSubscription<K extends keyof AllPubSubTypes>(
383440 sub : K ,
384441 ...args : Parameters < AllPubSubTypes [ K ] >
385442) : boolean {
386- const [ ready , setReady ] = useState < boolean > ( false )
443+ const { state : ready , setState : setReady , resetState : cancelPreviousReady } = useDelayState ( )
387444
388445 useEffect ( ( ) => {
389446 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+ )
391453 return ( ) => {
392454 isReadyComp . stop ( )
393455 setTimeout ( ( ) => {
394456 subscription . stop ( )
395- } , 1000 )
457+ } , SUBSCRIPTION_TIMEOUT )
458+ cancelPreviousReady ( SUBSCRIPTION_TIMEOUT )
396459 }
397460 } , [ sub , stringifyObjects ( args ) ] )
398461
399- return ready
462+ const isReady = ready
463+
464+ return isReady
400465}
401466
402467/**
@@ -415,7 +480,7 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
415480 enable : boolean ,
416481 ...args : Parameters < AllPubSubTypes [ K ] >
417482) : boolean {
418- const [ ready , setReady ] = useState < boolean > ( false )
483+ const { state : ready , setState : setReady , resetState : cancelPreviousReady } = useDelayState ( )
419484
420485 useEffect ( ( ) => {
421486 if ( ! enable ) {
@@ -424,16 +489,69 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
424489 }
425490
426491 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+ )
428498 return ( ) => {
429499 isReadyComp . stop ( )
430500 setTimeout ( ( ) => {
431501 subscription . stop ( )
432- } , 1000 )
502+ } , SUBSCRIPTION_TIMEOUT )
503+ cancelPreviousReady ( SUBSCRIPTION_TIMEOUT )
433504 }
434505 } , [ sub , enable , stringifyObjects ( args ) ] )
435506
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
437555}
438556
439557/**
0 commit comments