1
- import { MutableRefObject , useMemo , useRef } from 'react'
1
+ import { MutableRefObject , useEffect , useRef , useState } from 'react'
2
2
import useMounted from './useMounted'
3
- import useWillUnmount from './useWillUnmount'
4
3
5
4
/*
6
5
* Browsers including Internet Explorer, Chrome, Safari, and Firefox store the
@@ -28,12 +27,14 @@ function setChainedTimeout(
28
27
)
29
28
}
30
29
30
+ type TimeoutState = {
31
+ fn : ( ) => void
32
+ delayMs : number
33
+ }
31
34
/**
32
35
* Returns a controller object for setting a timeout that is properly cleaned up
33
36
* once the component unmounts. New timeouts cancel and replace existing ones.
34
37
*
35
- *
36
- *
37
38
* ```tsx
38
39
* const { set, clear } = useTimeout();
39
40
* const [hello, showHello] = useState(false);
@@ -47,33 +48,60 @@ function setChainedTimeout(
47
48
* ```
48
49
*/
49
50
export default function useTimeout ( ) {
51
+ const [ timeout , setTimeoutState ] = useState < TimeoutState | null > ( null )
50
52
const isMounted = useMounted ( )
51
53
52
54
// types are confused between node and web here IDK
53
- const handleRef = useRef < any > ( )
55
+ const handleRef = useRef < any > ( null )
54
56
55
- useWillUnmount ( ( ) => clearTimeout ( handleRef . current ) )
57
+ useEffect ( ( ) => {
58
+ if ( ! timeout ) {
59
+ return
60
+ }
56
61
57
- return useMemo ( ( ) => {
58
- const clear = ( ) => clearTimeout ( handleRef . current )
62
+ const { fn, delayMs } = timeout
59
63
60
- function set ( fn : ( ) => void , delayMs = 0 ) : void {
61
- if ( ! isMounted ( ) ) return
64
+ function task ( ) {
65
+ if ( isMounted ( ) ) {
66
+ setTimeoutState ( null )
67
+ }
68
+ fn ( )
69
+ }
62
70
63
- clear ( )
71
+ if ( delayMs <= MAX_DELAY_MS ) {
72
+ // For simplicity, if the timeout is short, just set a normal timeout.
73
+ handleRef . current = setTimeout ( task , delayMs )
74
+ } else {
75
+ setChainedTimeout ( handleRef , task , Date . now ( ) + delayMs )
76
+ }
77
+ const handle = handleRef . current
64
78
65
- if ( delayMs <= MAX_DELAY_MS ) {
66
- // For simplicity, if the timeout is short, just set a normal timeout.
67
- handleRef . current = setTimeout ( fn , delayMs )
68
- } else {
69
- setChainedTimeout ( handleRef , fn , Date . now ( ) + delayMs )
79
+ return ( ) => {
80
+ // this should be a no-op since they are either the same or `handle`
81
+ // already expired but no harm in calling twice
82
+ if ( handleRef . current !== handle ) {
83
+ clearTimeout ( handle )
70
84
}
85
+
86
+ clearTimeout ( handleRef . current )
87
+ handleRef . current === null
71
88
}
89
+ } , [ timeout ] )
72
90
91
+ const [ returnValue ] = useState ( ( ) => {
73
92
return {
74
- set,
75
- clear,
93
+ set ( fn : ( ) => void , delayMs = 0 ) : void {
94
+ if ( ! isMounted ( ) ) return
95
+
96
+ setTimeoutState ( { fn, delayMs } )
97
+ } ,
98
+ clear ( ) {
99
+ setTimeoutState ( null )
100
+ } ,
101
+ isPending : ! ! timeout ,
76
102
handleRef,
77
103
}
78
- } , [ ] )
104
+ } )
105
+
106
+ return returnValue
79
107
}
0 commit comments