Repository navigation
Fix useDebouncedEvent pending calls surviving unmount and delay changes - #11400
Merged
Merged
Conversation
Co-authored-by: fzaninotto <99944+fzaninotto@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix useDebouncedEvent to allow cancellation and proper cleanup
Fix Oct 11, 2026
useDebouncedEvent pending calls surviving unmount and delay changes, and expose cancel/flush
fzaninotto
marked this pull request as ready for review
October 13, 2026 15:54
Member
|
@copilot fix the ci failures |
Co-authored-by: fzaninotto <99944+fzaninotto@users.noreply.github.com>
…elay change Cancelling dropped user input in FilterLiveForm: a filter typed just before the form unmounts (or before its debounce changes) was never applied, because setFilters lives in the ListContext which outlives the form. Callers that want to drop a pending call can still call cancel() explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dash's Exposing lodash's DebouncedFunc tied ra-core's public API to @types/lodash. A local DebouncedEvent interface also removes the need for the `as unknown as` cast on the placeholder ref. Tests now use fireEvent so clicks are wrapped in act(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useDebouncedEvent pending calls surviving unmount and delay changes, and expose cancel/flushuseDebouncedEvent pending calls surviving unmount and delay changes
fzaninotto
approved these changes
Oct 16, 2026
Comp did not say what it renders, and its `any` props hid type errors in the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ush() A cancelOnClick prop forced the cancel test to rerender mid-test to switch the button's behavior. Separate update/cancel/flush buttons read like the real use case (cancel from an event handler) and let flush() be tested directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fzaninotto
approved these changes
Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
useDebouncedEventwrapslodash/debouncebut never cleans up the instance it creates, and it hidescancel()andflush(). As a result:delayms later, after the component is gone.delaychange replaces the debounced instance but leaves the old one's pending call to fire later.Solution
delaychange, the pending call runs immediately instead ofdelayms later.cancel/flush: the returned callback is still referentially stable, and now carriescancel()andflush(). Both delegate to the current debounced instance.DebouncedEvent<Args, Return>interface, so ra-core's public API does not depend on lodash typings.This is backward compatible: the return value is still callable with the same signature, and only gains two properties.
How To Test
Run
yarn jest packages/ra-core/src/util/useDebouncedEvent. The spec uses fake timers to check:cancel()drops a pending callflush()runs a pending call immediately, and only oncedelaychange flushes a pending callAdditional Checks
masterfor a bugfix or a documentation fix, ornextfor a featurecancel/flushare not documented yet.Also, please make sure to read the contributing guidelines.
useDebouncedEvent: pending calls can't be cancelled, and survive unmount #11399What & why:
useDebouncedEventnow exposescancel()/flush()and flushes a pending call on unmount ordelaychange, so callers can drop pending calls and no call fires late.Risk: 🔴 High (public API of ra-core: hook return type and cleanup behavior change)
Start with: the
useLayoutEffectcleanup inpackages/ra-core/src/util/useDebouncedEvent.ts, which decides what happens to a pending call.🎯 Intent
Source: #11399 (provided by dev)
🧭 Non-obvious decisions
FilterLiveForm'ssetFilterslives in the ListContext, which outlives the form. Cancelling dropped a filter typed less thandebouncems before the filter form unmounts. Flushing runs the call during unmount, neverdelayms after.cancel()from an event handler: acancel()in the caller's own cleanup runs after the hook's flush.DebouncedEventinterface instead of lodash'sDebouncedFunc: keeps@types/lodashout of ra-core's public types and removes theas unknown ascast on the placeholder ref.🧪 Tests
yarn jest packages/ra-core/src/util/useDebouncedEvent: 1 suite, 5 tests passedyarn jest packages/ra-core/src/util/useDebouncedEvent packages/ra-core/src/form/FilterLiveForm: 2 suites passed (before the spec refactor)yarn jest packages/ra-core/src/util/useDebouncedEvent packages/ra-core/src/form/FilterLiveForm packages/ra-ui-materialui/src/list/filter(before the type refactor): 8 suites, 56 tests passednpx tsc --noEmit -p packages/ra-coreandnpx eslinton both files: no errors💥 Impact if wrong
FilterLiveForm(all list filter forms), and apps that importuseDebouncedEvent📖 Docs to update
useDebouncedEventhas no page indocs/ordocs_headless/;cancel/flushare undocumented.🤖 AI
git fetch origin copilot/fix-usedebouncedevent-cancel-flush && git switch copilot/fix-usedebouncedevent-cancel-flushyarn install && yarn jest packages/ra-core/src/util/useDebouncedEvent packages/ra-core/src/form/FilterLiveForm packages/ra-ui-materialui/src/list/filter && npx tsc --noEmit -p packages/ra-coreFilterLiveForm's watch unsubscribe, sosetFiltersmust still be safe to call at that point.🤖 Generated with Claude Code