1- import { useRef , useCallback , useEffect , useMemo } from 'react' ;
1+ import { useRef , useEffect , useMemo } from 'react' ;
22
33export interface CallOptions {
44 leading ?: boolean ;
@@ -20,7 +20,7 @@ export interface ControlFunctions {
2020 * Note, that if there are no previous invocations it's mean you will get undefined. You should check it in your code properly.
2121 */
2222export interface DebouncedState < T extends ( ...args : any [ ] ) => ReturnType < T > > extends ControlFunctions {
23- callback : ( ...args : Parameters < T > ) => ReturnType < T > ;
23+ ( ...args : Parameters < T > ) : ReturnType < T > ;
2424}
2525
2626/**
@@ -54,12 +54,12 @@ export interface DebouncedState<T extends (...args: any[]) => ReturnType<T>> ext
5454 * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
5555 * used (if available, otherwise it will be setTimeout(...,0)).
5656 * @param {Object } [options={}] The options object.
57- * @param {boolean } [options.leading=false]
5857 * Specify invoking on the leading edge of the timeout.
59- * @param {number } [options.maxWait ]
58+ * @param {boolean } [options.leading=false ]
6059 * The maximum time `func` is allowed to be delayed before it's invoked.
61- * @param {boolean } [options.trailing=true ]
60+ * @param {number } [options.maxWait ]
6261 * Specify invoking on the trailing edge of the timeout.
62+ * @param {boolean } [options.trailing=true]
6363 * @returns {Function } Returns the new debounced function.
6464 * @example
6565 *
@@ -94,10 +94,11 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
9494 const lastInvokeTime = useRef ( 0 ) ;
9595 const timerId = useRef ( null ) ;
9696 const lastArgs = useRef < unknown [ ] > ( [ ] ) ;
97- const lastThis = useRef ( ) ;
98- const result = useRef ( ) ;
97+ const lastThis = useRef < unknown > ( ) ;
98+ const result = useRef < ReturnType < T > > ( ) ;
9999 const funcRef = useRef ( func ) ;
100100 const mounted = useRef ( true ) ;
101+
101102 funcRef . current = func ;
102103
103104 // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
@@ -115,25 +116,39 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
115116 const maxing = 'maxWait' in options ;
116117 const maxWait = maxing ? Math . max ( + options . maxWait || 0 , wait ) : null ;
117118
118- const invokeFunc = useCallback ( ( time ) => {
119- const args = lastArgs . current ;
120- const thisArg = lastThis . current ;
121-
122- lastArgs . current = lastThis . current = null ;
123- lastInvokeTime . current = time ;
124- return ( result . current = funcRef . current . apply ( thisArg , args ) ) ;
119+ useEffect ( ( ) => {
120+ mounted . current = true ;
121+ return ( ) => {
122+ mounted . current = false ;
123+ } ;
125124 } , [ ] ) ;
126125
127- const startTimer = useCallback (
128- ( pendingFunc , wait ) => {
126+ // You may have a question, why we have so many code under the useMemo definition.
127+ //
128+ // This was made as we want to escape from useCallback hell and
129+ // not to initialize a number of functions each time useDebouncedCallback is called.
130+ //
131+ // It means that we have less garbage for our GC calls which improves performance.
132+ // Also, it makes this library smaller.
133+ //
134+ // And the last reason, that the code without lots of useCallback with deps is easier to read.
135+ // You have only one place for that.
136+ const debounced = useMemo ( ( ) => {
137+ const invokeFunc = ( time : number ) => {
138+ const args = lastArgs . current ;
139+ const thisArg = lastThis . current ;
140+
141+ lastArgs . current = lastThis . current = null ;
142+ lastInvokeTime . current = time ;
143+ return ( result . current = funcRef . current . apply ( thisArg , args ) ) ;
144+ } ;
145+
146+ const startTimer = ( pendingFunc : ( ) => void , wait : number ) => {
129147 if ( useRAF ) cancelAnimationFrame ( timerId . current ) ;
130148 timerId . current = useRAF ? requestAnimationFrame ( pendingFunc ) : setTimeout ( pendingFunc , wait ) ;
131- } ,
132- [ useRAF ]
133- ) ;
149+ } ;
134150
135- const shouldInvoke = useCallback (
136- ( time ) => {
151+ const shouldInvoke = ( time : number ) => {
137152 if ( ! mounted . current ) return false ;
138153
139154 const timeSinceLastCall = time - lastCallTime . current ;
@@ -148,12 +163,9 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
148163 timeSinceLastCall < 0 ||
149164 ( maxing && timeSinceLastInvoke >= maxWait )
150165 ) ;
151- } ,
152- [ maxWait , maxing , wait ]
153- ) ;
166+ } ;
154167
155- const trailingEdge = useCallback (
156- ( time ) => {
168+ const trailingEdge = ( time : number ) => {
157169 timerId . current = null ;
158170
159171 // Only invoke if we have `lastArgs` which means `func` has been
@@ -163,50 +175,28 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
163175 }
164176 lastArgs . current = lastThis . current = null ;
165177 return result . current ;
166- } ,
167- [ invokeFunc , trailing ]
168- ) ;
169-
170- const timerExpired = useCallback ( ( ) => {
171- const time = Date . now ( ) ;
172- if ( shouldInvoke ( time ) ) {
173- return trailingEdge ( time ) ;
174- }
175- // https://github.com/xnimorz/use-debounce/issues/97
176- if ( ! mounted . current ) {
177- return ;
178- }
179- // Remaining wait calculation
180- const timeSinceLastCall = time - lastCallTime . current ;
181- const timeSinceLastInvoke = time - lastInvokeTime . current ;
182- const timeWaiting = wait - timeSinceLastCall ;
183- const remainingWait = maxing ? Math . min ( timeWaiting , maxWait - timeSinceLastInvoke ) : timeWaiting ;
184-
185- // Restart the timer
186- startTimer ( timerExpired , remainingWait ) ;
187- } , [ maxWait , maxing , shouldInvoke , startTimer , trailingEdge , wait ] ) ;
188-
189- const cancel = useCallback ( ( ) => {
190- if ( timerId . current ) {
191- useRAF ? cancelAnimationFrame ( timerId . current ) : clearTimeout ( timerId . current ) ;
192- }
193- lastInvokeTime . current = 0 ;
194- lastArgs . current = lastCallTime . current = lastThis . current = timerId . current = null ;
195- } , [ useRAF ] ) ;
196-
197- const flush = useCallback ( ( ) => {
198- return ! timerId . current ? result . current : trailingEdge ( Date . now ( ) ) ;
199- } , [ trailingEdge ] ) ;
178+ } ;
200179
201- useEffect ( ( ) => {
202- mounted . current = true ;
203- return ( ) => {
204- mounted . current = false ;
180+ const timerExpired = ( ) => {
181+ const time = Date . now ( ) ;
182+ if ( shouldInvoke ( time ) ) {
183+ return trailingEdge ( time ) ;
184+ }
185+ // https://github.com/xnimorz/use-debounce/issues/97
186+ if ( ! mounted . current ) {
187+ return ;
188+ }
189+ // Remaining wait calculation
190+ const timeSinceLastCall = time - lastCallTime . current ;
191+ const timeSinceLastInvoke = time - lastInvokeTime . current ;
192+ const timeWaiting = wait - timeSinceLastCall ;
193+ const remainingWait = maxing ? Math . min ( timeWaiting , maxWait - timeSinceLastInvoke ) : timeWaiting ;
194+
195+ // Restart the timer
196+ startTimer ( timerExpired , remainingWait ) ;
205197 } ;
206- } , [ ] ) ;
207198
208- const debounced = useCallback (
209- ( ...args : Parameters < T > ) : ReturnType < T > => {
199+ const func : DebouncedState < T > = ( ...args : Parameters < T > ) : ReturnType < T > => {
210200 const time = Date . now ( ) ;
211201 const isInvoking = shouldInvoke ( time ) ;
212202
@@ -233,23 +223,26 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
233223 startTimer ( timerExpired , wait ) ;
234224 }
235225 return result . current ;
236- } ,
237- [ invokeFunc , leading , maxing , shouldInvoke , startTimer , timerExpired , wait ]
238- ) ;
226+ } ;
239227
240- const isPending = useCallback ( ( ) => {
241- return ! ! timerId . current ;
242- } , [ ] ) ;
228+ func . cancel = ( ) => {
229+ if ( timerId . current ) {
230+ useRAF ? cancelAnimationFrame ( timerId . current ) : clearTimeout ( timerId . current ) ;
231+ }
232+ lastInvokeTime . current = 0 ;
233+ lastArgs . current = lastCallTime . current = lastThis . current = timerId . current = null ;
234+ } ;
235+
236+ func . isPending = ( ) => {
237+ return ! ! timerId . current ;
238+ } ;
239+
240+ func . flush = ( ) => {
241+ return ! timerId . current ? result . current : trailingEdge ( Date . now ( ) ) ;
242+ } ;
243+
244+ return func ;
245+ } , [ leading , maxing , wait , maxWait , trailing , useRAF ] ) ;
243246
244- const debouncedState : DebouncedState < T > = useMemo (
245- ( ) => ( {
246- callback : debounced ,
247- cancel,
248- flush,
249- isPending,
250- } ) ,
251- [ debounced , cancel , flush , isPending ]
252- ) ;
253-
254- return debouncedState ;
247+ return debounced ;
255248}
0 commit comments