From 42e9a6eb22733fd58d325fff9f552afb454c34d8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 11 Oct 2026 11:56:02 +0000
Subject: [PATCH 1/7] Initial plan
From 0f9c0bce4971d17d11aabd9d4be91c56139c62dc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 11 Oct 2026 11:57:59 +0000
Subject: [PATCH 2/7] Apply remaining changes
Co-authored-by: fzaninotto <99944+fzaninotto@users.noreply.github.com>
---
.../src/util/useDebouncedEvent.spec.tsx | 44 +++++++++++++++++++
.../ra-core/src/util/useDebouncedEvent.ts | 33 ++++++++++----
2 files changed, 69 insertions(+), 8 deletions(-)
create mode 100644 packages/ra-core/src/util/useDebouncedEvent.spec.tsx
diff --git a/packages/ra-core/src/util/useDebouncedEvent.spec.tsx b/packages/ra-core/src/util/useDebouncedEvent.spec.tsx
new file mode 100644
index 00000000000..979b99615dd
--- /dev/null
+++ b/packages/ra-core/src/util/useDebouncedEvent.spec.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import { render, screen, act } from '@testing-library/react';
+import { useDebouncedEvent } from './useDebouncedEvent';
+
+const Comp = ({ cb, cancelOnClick = false }: any) => {
+ const update = useDebouncedEvent(cb, 300);
+ return (
+
+ );
+};
+
+describe('useDebouncedEvent', () => {
+ beforeEach(() => jest.useFakeTimers());
+ afterEach(() => jest.useRealTimers());
+
+ it('calls the callback after the delay', () => {
+ const cb = jest.fn();
+ render();
+ screen.getByText('go').click();
+ act(() => jest.advanceTimersByTime(300));
+ expect(cb).toHaveBeenCalledWith('x');
+ });
+ it('cancels a pending call with cancel()', () => {
+ const cb = jest.fn();
+ const { rerender } = render();
+ screen.getByText('go').click();
+ rerender();
+ screen.getByText('go').click();
+ act(() => jest.advanceTimersByTime(300));
+ expect(cb).not.toHaveBeenCalled();
+ });
+ it('cancels a pending call on unmount', () => {
+ const cb = jest.fn();
+ const { unmount } = render();
+ screen.getByText('go').click();
+ unmount();
+ act(() => jest.advanceTimersByTime(300));
+ expect(cb).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/ra-core/src/util/useDebouncedEvent.ts b/packages/ra-core/src/util/useDebouncedEvent.ts
index abd608e75fb..80a8a6898a3 100644
--- a/packages/ra-core/src/util/useDebouncedEvent.ts
+++ b/packages/ra-core/src/util/useDebouncedEvent.ts
@@ -1,6 +1,7 @@
import * as React from 'react';
-import { useCallback, useRef } from 'react';
+import { useMemo, useRef } from 'react';
import debounce from 'lodash/debounce.js';
+import type { DebouncedFunc } from 'lodash';
import { useEvent } from './useEvent';
// allow the hook to work in SSR
@@ -19,10 +20,17 @@ export const useDebouncedEvent = (
delay: number
) => {
// Create a ref that stores the debounced callback
- const debouncedCallbackRef = useRef<(...args: Args) => Return | undefined>(
- () => {
- throw new Error('Cannot call an event handler while rendering.');
- }
+ const debouncedCallbackRef = useRef Return
+ >>(
+ Object.assign(
+ () => {
+ throw new Error(
+ 'Cannot call an event handler while rendering.'
+ );
+ },
+ { cancel: () => {}, flush: () => undefined }
+ ) as unknown as DebouncedFunc<(...args: Args) => Return>
);
// Keep a stable ref to the callback (in case it's an inline function for instance)
@@ -30,13 +38,22 @@ export const useDebouncedEvent = (
// Whenever callback or delay changes, we need to update the debounced callback
useLayoutEffect(() => {
- debouncedCallbackRef.current = debounce(stableCallback, delay);
+ const debounced = debounce(stableCallback, delay);
+ debouncedCallbackRef.current = debounced;
+ return () => debounced.cancel();
}, [stableCallback, delay]);
// The function returned by useCallback will invoke the debounced callback
// Its dependencies array is empty, so it never changes across re-renders
- return useCallback(
- (...args: Args) => debouncedCallbackRef.current(...args),
+ return useMemo(
+ () =>
+ Object.assign(
+ (...args: Args) => debouncedCallbackRef.current(...args),
+ {
+ cancel: () => debouncedCallbackRef.current.cancel(),
+ flush: () => debouncedCallbackRef.current.flush(),
+ }
+ ),
[]
);
};
From 88cba47f2a01dc3c8c7c028a007a903d053f8642 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 13 Oct 2026 16:54:58 +0000
Subject: [PATCH 3/7] fix: Fix lint errors in useDebouncedEvent
Co-authored-by: fzaninotto <99944+fzaninotto@users.noreply.github.com>
---
packages/ra-core/src/util/useDebouncedEvent.spec.tsx | 4 +---
packages/ra-core/src/util/useDebouncedEvent.ts | 11 ++++++-----
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/packages/ra-core/src/util/useDebouncedEvent.spec.tsx b/packages/ra-core/src/util/useDebouncedEvent.spec.tsx
index 979b99615dd..683ff82d391 100644
--- a/packages/ra-core/src/util/useDebouncedEvent.spec.tsx
+++ b/packages/ra-core/src/util/useDebouncedEvent.spec.tsx
@@ -5,9 +5,7 @@ import { useDebouncedEvent } from './useDebouncedEvent';
const Comp = ({ cb, cancelOnClick = false }: any) => {
const update = useDebouncedEvent(cb, 300);
return (
-