Skip to content

Commit

Permalink
fix(useDebouncedCallback): make invoked function to be updated with d…
Browse files Browse the repository at this point in the history
…eps (#1510)

fix: #1357
  • Loading branch information
xobotyi committed Feb 4, 2024
1 parent a2a310e commit 12658ee
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/useDebouncedCallback/__docs__/story.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export function useDebouncedCallback<Args extends any[], This>(
#### Arguments

- **callback** _`(...args: T) => unknown`_ - function that will be debounced.
- **deps** _`React.DependencyList`_ - dependencies list when to update callback.
- **deps** _`React.DependencyList`_ - dependencies list when to update callback. It also replaces
invoked callback for scheduled debounced invocations.
- **delay** _`number`_ - debounce delay.
- **maxWait** _`number`_ _(default: `0`)_ - The maximum time `callback` is allowed to be delayed
before it's invoked. `0` means no max wait.
28 changes: 28 additions & 0 deletions src/useDebouncedCallback/__tests__/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,32 @@ describe('useDebouncedCallback', () => {
jest.advanceTimersByTime(200);
expect(cb).toHaveBeenCalledTimes(1);
});

it('should call updated function only when deps changed', () => {
const cb = jest.fn();

const { result, rerender } = renderHook(
({ cb, deps }: { cb: () => void; deps: any[] }) => useDebouncedCallback(cb, deps, 200, 200),
{
initialProps: {
cb() {},
deps: [0],
},
}
);

result.current();

rerender({ cb, deps: [0] });

jest.advanceTimersByTime(200);
expect(cb).toHaveBeenCalledTimes(0);

result.current();

rerender({ cb, deps: [1] });

jest.advanceTimersByTime(200);
expect(cb).toHaveBeenCalledTimes(1);
});
});
17 changes: 12 additions & 5 deletions src/useDebouncedCallback/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type DependencyList, useMemo, useRef } from 'react';
import { type DependencyList, useEffect, useMemo, useRef } from 'react';
import { useUnmountEffect } from '../useUnmountEffect/index.js';

export type DebouncedFunction<Fn extends (...args: any[]) => any> = (
Expand All @@ -10,7 +10,8 @@ export type DebouncedFunction<Fn extends (...args: any[]) => any> = (
* Makes passed function debounced, otherwise acts like `useCallback`.
*
* @param callback Function that will be debounced.
* @param deps Dependencies list when to update callback.
* @param deps Dependencies list when to update callback. It also replaces invoked
* callback for scheduled debounced invocations.
* @param delay Debounce delay.
* @param maxWait The maximum time `callback` is allowed to be delayed before
* it's invoked. 0 means no max wait.
Expand All @@ -23,6 +24,7 @@ export function useDebouncedCallback<Fn extends (...args: any[]) => any>(
): DebouncedFunction<Fn> {
const timeout = useRef<ReturnType<typeof setTimeout>>();
const waitTimeout = useRef<ReturnType<typeof setTimeout>>();
const cb = useRef(callback);
const lastCall = useRef<{ args: Parameters<Fn>; this: ThisParameterType<Fn> }>();

const clear = () => {
Expand All @@ -40,18 +42,23 @@ export function useDebouncedCallback<Fn extends (...args: any[]) => any>(
// Cancel scheduled execution on unmount
useUnmountEffect(clear);

useEffect(() => {
cb.current = callback;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);

return useMemo(() => {
const execute = () => {
clear();

// Barely possible to test this line
/* istanbul ignore next */
if (!lastCall.current) return;

const context = lastCall.current;
lastCall.current = undefined;

callback.apply(context.this, context.args);

clear();
cb.current.apply(context.this, context.args);
};

const wrapped = function (this, ...args) {
Expand Down

0 comments on commit 12658ee

Please sign in to comment.