Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(useDebouncedCallback): make invoked function to be updated with deps #1510

Merged
merged 1 commit into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading