Skip to content

Commit 045f960

Browse files
committed
make return result callable, get rid of useCallbacks
1 parent 43c3155 commit 045f960

5 files changed

Lines changed: 170 additions & 141 deletions

File tree

CHANGELOG.md

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
## 6.0.0
22

3+
- _breakind change_: removed `callback` field, instead of this `useDebouncedCallback` and `useThrottledCallback` returns a callable function:
4+
Old:
5+
6+
```js
7+
const { callback, pending } = useDebouncedCallback(/*...*/);
8+
// ...
9+
debounced.callback();
10+
```
11+
12+
New:
13+
14+
```js
15+
const debounced = useDebouncedCallback(/*...*/);
16+
// ...
17+
debounced();
18+
/**
19+
* Also debounced has fields:
20+
* {
21+
* cancel: () => void
22+
* flush: () => void
23+
* isPending: () => boolean
24+
* }
25+
* So you can call debounced.cancel(), debounced.flush(), debounced.isPending()
26+
*/
27+
```
28+
It makes easier to understand which cancel \ flush or isPending is called in case you have several debounced functions in your component
29+
330
- _breaking change_: Now `useDebounce`, `useDebouncedCallback` and `useThrottledCallback` has `isPending` method instead of `pending`
431

532
Old:
633

734
```js
8-
const {callback, pending} = useDebouncedCallback(/*...*/);
35+
const { callback, pending } = useDebouncedCallback(/*...*/);
936
```
1037

1138
New:
@@ -22,6 +49,12 @@
2249
*/
2350
```
2451

52+
- get rid of `useCallback` calls
53+
54+
- improve internal typing
55+
56+
- decrease the amount of functions to initialize each `useDebouncedCallback` call
57+
2558
## 5.2.1
2659

2760
- prevent having ininite setTimeout setup when component gets unmounted https://github.com/xnimorz/use-debounce/issues/97
@@ -51,32 +84,35 @@
5184

5285
- Reduce bundle size (thanks to [@omgovich](https://github.com/omgovich)):
5386
Before:
87+
5488
```
55-
esm/index.js
89+
esm/index.js
5690
Size: 908 B with all dependencies, minified and gzipped
5791
58-
esm/index.js
92+
esm/index.js
5993
Size: 873 B with all dependencies, minified and gzipped
6094
61-
esm/index.js
95+
esm/index.js
6296
Size: 755 B with all dependencies, minified and gzipped
6397
```
98+
6499
Now:
100+
65101
```
66-
esm/index.js
102+
esm/index.js
67103
Size: 826 B with all dependencies, minified and gzipped
68-
69-
esm/index.js
104+
105+
esm/index.js
70106
Size: 790 B with all dependencies, minified and gzipped
71-
72-
esm/index.js
107+
108+
esm/index.js
73109
Size: 675 B with all dependencies, minified and gzipped
74110
```
75111

76112
- Add notes about returned value from `debounced.callback` and its subsequent calls: https://github.com/xnimorz/use-debounce#returned-value-from-debouncedcallback
77113

78114
- Add project logo (thanks to [@omgovich](https://github.com/omgovich)):
79-
<img src="logo.png" width="500" alt="use-debounce" />
115+
<img src="logo.png" width="500" alt="use-debounce" />
80116

81117
## 5.0.1
82118

src/useDebounce.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function useDebounce<T>(
2929
useEffect(() => {
3030
// We need to use this condition otherwise we will run debounce timer for the first render (including maxWait option)
3131
if (!eq(previousValue.current, value)) {
32-
debounced.callback(value);
32+
debounced(value);
3333
previousValue.current = value;
3434
}
3535
}, [value, debounced, eq]);

src/useDebouncedCallback.ts

Lines changed: 77 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef, useCallback, useEffect, useMemo } from 'react';
1+
import { useRef, useEffect, useMemo } from 'react';
22

33
export 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
*/
2222
export 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

Comments
 (0)