Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,10 @@ describe('@mantine/hooks/use-debounced-callback', () => {
expect(callback).not.toHaveBeenCalled();

jest.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledWith(3);
expect(callback).not.toHaveBeenCalled(); // Fixed: no trailing call expected
});

it('resets leading after flush', () => {
it('resets leading after delay with leading=true', () => {
const callback = jest.fn();
const { result } = renderHook(() =>
useDebouncedCallback(callback, { delay: 100, leading: true })
Expand All @@ -188,20 +188,18 @@ describe('@mantine/hooks/use-debounced-callback', () => {
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('a');

// A second call is made. Since "leadingRef" is now false, this call is debounced and schedules a timeout.
// A second call during delay period should be ignored
result.current('b');
// The callback has still only been called once (with 'a').
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1); // Still only 'a'

// Then we advance the timers to trigger the internal flush of the first call, executing "b"
// After delay, no trailing execution should happen
jest.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenNthCalledWith(2, 'b');
expect(callback).toHaveBeenCalledTimes(1); // Still only 'a'

// After the flush from "b", "leadingRef" resets, so the next call fires immediately again
// After delay has passed, leading state should reset - next call fires immediately
result.current('c');
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenCalledWith('c');
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenNthCalledWith(2, 'c');
});

it('doesnt call on leading edge if leading changes from true to false', () => {
Expand All @@ -227,13 +225,14 @@ describe('@mantine/hooks/use-debounced-callback', () => {
expect(callback).toHaveBeenCalledTimes(1);
rerender({ delay: 200 });
result.current(2);
expect(callback).toHaveBeenCalledTimes(2); // Leading call

result.current(3);
expect(callback).toHaveBeenCalledTimes(2);
jest.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledTimes(2);
jest.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenCalledWith(3);
expect(callback).toHaveBeenCalledTimes(2);
});

it('can cancel debounced callback', () => {
Expand Down Expand Up @@ -334,4 +333,25 @@ describe('@mantine/hooks/use-debounced-callback', () => {
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(2);
});

it('leading=true should suppress trailing execution', () => {
const callback = jest.fn();
const { result } = renderHook(() =>
useDebouncedCallback(callback, { delay: 100, leading: true })
);
result.current('first');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('first');

result.current('second');
expect(callback).toHaveBeenCalledTimes(1); // Still only first call

callback.mockClear();
jest.advanceTimersByTime(100);
expect(callback).not.toHaveBeenCalled(); // This is the fix

result.current('third');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('third');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,56 @@ export function useDebouncedCallback<T extends (...args: any[]) => any>(
const isFirstCall = currentCallback._isFirstCall;
currentCallback._isFirstCall = false;

function clearTimeoutAndLeadingRef() {
window.clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = 0;
currentCallback._isFirstCall = true;
}

if (leading && isFirstCall) {
handleCallback(...args);

const resetLeadingState = () => {
clearTimeoutAndLeadingRef();
};

const flush = () => {
if (debounceTimerRef.current !== 0) {
clearTimeoutAndLeadingRef();
handleCallback(...args);
}
};

const cancel = () => {
clearTimeoutAndLeadingRef();
};

currentCallback.flush = flush;
currentCallback.cancel = cancel;
debounceTimerRef.current = window.setTimeout(resetLeadingState, delay);
return;
}

function clearTimeoutAndLeadingRef() {
window.clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = 0;
currentCallback._isFirstCall = true;
if (leading && !isFirstCall) {
const flush = () => {
if (debounceTimerRef.current !== 0) {
clearTimeoutAndLeadingRef();
handleCallback(...args);
}
};

const cancel = () => {
clearTimeoutAndLeadingRef();
};

currentCallback.flush = flush;
currentCallback.cancel = cancel;

const resetLeadingState = () => {
clearTimeoutAndLeadingRef();
};
debounceTimerRef.current = window.setTimeout(resetLeadingState, delay);
return;
}

const flush = () => {
Expand All @@ -57,7 +98,11 @@ export function useDebouncedCallback<T extends (...args: any[]) => any>(
currentCallback.cancel = cancel;
debounceTimerRef.current = window.setTimeout(flush, delay);
},
{ flush: () => {}, cancel: () => {}, _isFirstCall: true }
{
flush: () => {},
cancel: () => {},
_isFirstCall: true,
}
);
return currentCallback;
}, [handleCallback, delay, leading]);
Expand Down