Skip to content

Commit

Permalink
make return result callable, get rid of useCallbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
xnimorz committed Mar 7, 2021
1 parent 43c3155 commit 045f960
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 141 deletions.
56 changes: 46 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
## 6.0.0

- _breakind change_: removed `callback` field, instead of this `useDebouncedCallback` and `useThrottledCallback` returns a callable function:
Old:

```js
const { callback, pending } = useDebouncedCallback(/*...*/);
// ...
debounced.callback();
```

New:

```js
const debounced = useDebouncedCallback(/*...*/);
// ...
debounced();
/**
* Also debounced has fields:
* {
* cancel: () => void
* flush: () => void
* isPending: () => boolean
* }
* So you can call debounced.cancel(), debounced.flush(), debounced.isPending()
*/
```
It makes easier to understand which cancel \ flush or isPending is called in case you have several debounced functions in your component

- _breaking change_: Now `useDebounce`, `useDebouncedCallback` and `useThrottledCallback` has `isPending` method instead of `pending`

Old:

```js
const {callback, pending} = useDebouncedCallback(/*...*/);
const { callback, pending } = useDebouncedCallback(/*...*/);
```

New:
Expand All @@ -22,6 +49,12 @@
*/
```

- get rid of `useCallback` calls

- improve internal typing

- decrease the amount of functions to initialize each `useDebouncedCallback` call

## 5.2.1

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

- Reduce bundle size (thanks to [@omgovich](https://github.com/omgovich)):
Before:

```
esm/index.js
esm/index.js
Size: 908 B with all dependencies, minified and gzipped
esm/index.js
esm/index.js
Size: 873 B with all dependencies, minified and gzipped
esm/index.js
esm/index.js
Size: 755 B with all dependencies, minified and gzipped
```

Now:

```
esm/index.js
esm/index.js
Size: 826 B with all dependencies, minified and gzipped
esm/index.js
esm/index.js
Size: 790 B with all dependencies, minified and gzipped
esm/index.js
esm/index.js
Size: 675 B with all dependencies, minified and gzipped
```

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

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

## 5.0.1

Expand Down
2 changes: 1 addition & 1 deletion src/useDebounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function useDebounce<T>(
useEffect(() => {
// We need to use this condition otherwise we will run debounce timer for the first render (including maxWait option)
if (!eq(previousValue.current, value)) {
debounced.callback(value);
debounced(value);
previousValue.current = value;
}
}, [value, debounced, eq]);
Expand Down
161 changes: 77 additions & 84 deletions src/useDebouncedCallback.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useCallback, useEffect, useMemo } from 'react';
import { useRef, useEffect, useMemo } from 'react';

export interface CallOptions {
leading?: boolean;
Expand All @@ -20,7 +20,7 @@ export interface ControlFunctions {
* Note, that if there are no previous invocations it's mean you will get undefined. You should check it in your code properly.
*/
export interface DebouncedState<T extends (...args: any[]) => ReturnType<T>> extends ControlFunctions {
callback: (...args: Parameters<T>) => ReturnType<T>;
(...args: Parameters<T>): ReturnType<T>;
}

/**
Expand Down Expand Up @@ -54,12 +54,12 @@ export interface DebouncedState<T extends (...args: any[]) => ReturnType<T>> ext
* The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
* used (if available, otherwise it will be setTimeout(...,0)).
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=false]
* Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait]
* @param {boolean} [options.leading=false]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true]
* @param {number} [options.maxWait]
* Specify invoking on the trailing edge of the timeout.
* @param {boolean} [options.trailing=true]
* @returns {Function} Returns the new debounced function.
* @example
*
Expand Down Expand Up @@ -94,10 +94,11 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
const lastInvokeTime = useRef(0);
const timerId = useRef(null);
const lastArgs = useRef<unknown[]>([]);
const lastThis = useRef();
const result = useRef();
const lastThis = useRef<unknown>();
const result = useRef<ReturnType<T>>();
const funcRef = useRef(func);
const mounted = useRef(true);

funcRef.current = func;

// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
Expand All @@ -115,25 +116,39 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
const maxing = 'maxWait' in options;
const maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : null;

const invokeFunc = useCallback((time) => {
const args = lastArgs.current;
const thisArg = lastThis.current;

lastArgs.current = lastThis.current = null;
lastInvokeTime.current = time;
return (result.current = funcRef.current.apply(thisArg, args));
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);

const startTimer = useCallback(
(pendingFunc, wait) => {
// You may have a question, why we have so many code under the useMemo definition.
//
// This was made as we want to escape from useCallback hell and
// not to initialize a number of functions each time useDebouncedCallback is called.
//
// It means that we have less garbage for our GC calls which improves performance.
// Also, it makes this library smaller.
//
// And the last reason, that the code without lots of useCallback with deps is easier to read.
// You have only one place for that.
const debounced = useMemo(() => {
const invokeFunc = (time: number) => {
const args = lastArgs.current;
const thisArg = lastThis.current;

lastArgs.current = lastThis.current = null;
lastInvokeTime.current = time;
return (result.current = funcRef.current.apply(thisArg, args));
};

const startTimer = (pendingFunc: () => void, wait: number) => {
if (useRAF) cancelAnimationFrame(timerId.current);
timerId.current = useRAF ? requestAnimationFrame(pendingFunc) : setTimeout(pendingFunc, wait);
},
[useRAF]
);
};

const shouldInvoke = useCallback(
(time) => {
const shouldInvoke = (time: number) => {
if (!mounted.current) return false;

const timeSinceLastCall = time - lastCallTime.current;
Expand All @@ -148,12 +163,9 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
},
[maxWait, maxing, wait]
);
};

const trailingEdge = useCallback(
(time) => {
const trailingEdge = (time: number) => {
timerId.current = null;

// Only invoke if we have `lastArgs` which means `func` has been
Expand All @@ -163,50 +175,28 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
}
lastArgs.current = lastThis.current = null;
return result.current;
},
[invokeFunc, trailing]
);

const timerExpired = useCallback(() => {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// https://github.com/xnimorz/use-debounce/issues/97
if (!mounted.current) {
return;
}
// Remaining wait calculation
const timeSinceLastCall = time - lastCallTime.current;
const timeSinceLastInvoke = time - lastInvokeTime.current;
const timeWaiting = wait - timeSinceLastCall;
const remainingWait = maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;

// Restart the timer
startTimer(timerExpired, remainingWait);
}, [maxWait, maxing, shouldInvoke, startTimer, trailingEdge, wait]);

const cancel = useCallback(() => {
if (timerId.current) {
useRAF ? cancelAnimationFrame(timerId.current) : clearTimeout(timerId.current);
}
lastInvokeTime.current = 0;
lastArgs.current = lastCallTime.current = lastThis.current = timerId.current = null;
}, [useRAF]);

const flush = useCallback(() => {
return !timerId.current ? result.current : trailingEdge(Date.now());
}, [trailingEdge]);
};

useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
const timerExpired = () => {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// https://github.com/xnimorz/use-debounce/issues/97
if (!mounted.current) {
return;
}
// Remaining wait calculation
const timeSinceLastCall = time - lastCallTime.current;
const timeSinceLastInvoke = time - lastInvokeTime.current;
const timeWaiting = wait - timeSinceLastCall;
const remainingWait = maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;

// Restart the timer
startTimer(timerExpired, remainingWait);
};
}, []);

const debounced = useCallback(
(...args: Parameters<T>): ReturnType<T> => {
const func: DebouncedState<T> = (...args: Parameters<T>): ReturnType<T> => {
const time = Date.now();
const isInvoking = shouldInvoke(time);

Expand All @@ -233,23 +223,26 @@ export default function useDebouncedCallback<T extends (...args: any[]) => Retur
startTimer(timerExpired, wait);
}
return result.current;
},
[invokeFunc, leading, maxing, shouldInvoke, startTimer, timerExpired, wait]
);
};

const isPending = useCallback(() => {
return !!timerId.current;
}, []);
func.cancel = () => {
if (timerId.current) {
useRAF ? cancelAnimationFrame(timerId.current) : clearTimeout(timerId.current);
}
lastInvokeTime.current = 0;
lastArgs.current = lastCallTime.current = lastThis.current = timerId.current = null;
};

func.isPending = () => {
return !!timerId.current;
};

func.flush = () => {
return !timerId.current ? result.current : trailingEdge(Date.now());
};

return func;
}, [leading, maxing, wait, maxWait, trailing, useRAF]);

const debouncedState: DebouncedState<T> = useMemo(
() => ({
callback: debounced,
cancel,
flush,
isPending,
}),
[debounced, cancel, flush, isPending]
);

return debouncedState;
return debounced;
}
Loading

0 comments on commit 045f960

Please sign in to comment.