Skip to content

fix(useDebouncedCallback, useThrottledCallback): cancel the pending call when returning to the last forwarded value - #450

Merged
hyesungoh merged 2 commits into
mainfrom
fix/debounced-callback-stale-pending
Aug 28, 2026
Merged

fix(useDebouncedCallback, useThrottledCallback): cancel the pending call when returning to the last forwarded value#450
hyesungoh merged 2 commits into
mainfrom
fix/debounced-callback-stale-pending

Conversation

@hyesungoh

@hyesungoh hyesungoh commented Aug 28, 2026

Copy link
Copy Markdown
Member

Problem

Both useDebouncedCallback and useThrottledCallback skip a call whose value equals the last forwarded one — but they did so before cancelling the pending call, so a call for a different value scheduled in between survived and fired.

const search = useDebouncedCallback({ onChange: fetchResults, timeThreshold: 300 });

search('seoul');   // 300ms later → fetchResults('seoul')   ✓
search('seo');     // user deletes two chars → 'seo' scheduled
search('seoul');   // user types them back  → equals last forwarded, early return
                   //                          'seo' timer is NOT cancelled
                   // 300ms later → fetchResults('seo')      ✗  input shows 'seoul'

useThrottledCallback has the same ordering. With the default edges the leading edge forwards 'seo' immediately and updates the compared value, so the third call is not a duplicate and the defect is masked. With edges: ['trailing'] it reproduces exactly as above.

The comparison has been first in both callbacks since #90; #446 changed the type but not this ordering.

Fix

Move clearPreviousDebounce() / clearPreviousThrottle() above the dedupe check. Cancelling was already unconditional on every non-duplicate call, so the only behaviour change is that a duplicate call now also cancels whatever was pending.

Tests

One new test per hook, discards a pending (trailing) value when the caller returns to the last forwarded one. Each fails on main (onChange called 2 times, second with 'seo') and passes with the fix. All 521 tests pass, coverage stays at 100%.

Changeset

patch — behaviour fix, no API change.

…the last forwarded value

The dedupe check returned before clearPreviousDebounce(), so a call that
matched the last forwarded value left a pending call for a different value
alive. Typing 'seoul' → 'seo' → 'seoul' forwarded 'seo'.
@hyesungoh
hyesungoh requested a review from mnxmnz as a code owner August 28, 2026 04:58
Copilot AI lite review requested due to automatic review settings August 28, 2026 04:58
@changeset-bot

changeset-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4b62235

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
react-simplikit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (0b66d40) to head (632ebcf).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #450   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           58        58           
  Lines         1664      1664           
  Branches       500       500           
=========================================
  Hits          1664      1664           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Size Change: +7 B (+0.01%)

Total Size: 97.3 kB

📦 View Changed
Filename Size Change
packages/react-simplikit/dist/hooks/useDebouncedCallback/useDebouncedCallback.cjs 1.36 kB +3 B (+0.22%)
packages/react-simplikit/dist/hooks/useDebouncedCallback/useDebouncedCallback.mjs 1.34 kB +1 B (+0.07%)
packages/react-simplikit/dist/hooks/useThrottledCallback/useThrottledCallback.cjs 1.22 kB +2 B (+0.16%)
packages/react-simplikit/dist/hooks/useThrottledCallback/useThrottledCallback.mjs 1.19 kB +1 B (+0.08%)
ℹ️ View Unchanged
Filename Size
packages/react-simplikit/dist/components/ImpressionArea/ImpressionArea.cjs 1.01 kB
packages/react-simplikit/dist/components/ImpressionArea/ImpressionArea.mjs 984 B
packages/react-simplikit/dist/components/Separated/Separated.cjs 686 B
packages/react-simplikit/dist/components/Separated/Separated.mjs 684 B
packages/react-simplikit/dist/components/SwitchCase/SwitchCase.cjs 701 B
packages/react-simplikit/dist/components/SwitchCase/SwitchCase.mjs 699 B
packages/react-simplikit/dist/hooks/useAsyncEffect/useAsyncEffect.cjs 626 B
packages/react-simplikit/dist/hooks/useAsyncEffect/useAsyncEffect.mjs 615 B
packages/react-simplikit/dist/hooks/useBooleanState/useBooleanState.cjs 539 B
packages/react-simplikit/dist/hooks/useBooleanState/useBooleanState.mjs 529 B
packages/react-simplikit/dist/hooks/useCallbackOncePerRender/useCallbackOncePerRender.cjs 790 B
packages/react-simplikit/dist/hooks/useCallbackOncePerRender/useCallbackOncePerRender.mjs 762 B
packages/react-simplikit/dist/hooks/useConditionalEffect/useConditionalEffect.cjs 955 B
packages/react-simplikit/dist/hooks/useConditionalEffect/useConditionalEffect.mjs 934 B
packages/react-simplikit/dist/hooks/useControlledState/useControlledState.cjs 865 B
packages/react-simplikit/dist/hooks/useControlledState/useControlledState.mjs 855 B
packages/react-simplikit/dist/hooks/useCounter/useCounter.cjs 1.03 kB
packages/react-simplikit/dist/hooks/useCounter/useCounter.mjs 1.01 kB
packages/react-simplikit/dist/hooks/useDebounce/debounce.cjs 458 B
packages/react-simplikit/dist/hooks/useDebounce/debounce.mjs 456 B
packages/react-simplikit/dist/hooks/useDebounce/useDebounce.cjs 974 B
packages/react-simplikit/dist/hooks/useDebounce/useDebounce.mjs 953 B
packages/react-simplikit/dist/hooks/useDoubleClick/useDoubleClick.cjs 965 B
packages/react-simplikit/dist/hooks/useDoubleClick/useDoubleClick.mjs 950 B
packages/react-simplikit/dist/hooks/useGeolocation/useGeolocation.cjs 2.02 kB
packages/react-simplikit/dist/hooks/useGeolocation/useGeolocation.mjs 2.02 kB
packages/react-simplikit/dist/hooks/useImpressionRef/useImpressionRef.cjs 1.12 kB
packages/react-simplikit/dist/hooks/useImpressionRef/useImpressionRef.mjs 1.08 kB
packages/react-simplikit/dist/hooks/useInputState/useInputState.cjs 663 B
packages/react-simplikit/dist/hooks/useInputState/useInputState.mjs 654 B
packages/react-simplikit/dist/hooks/useIntersectionObserver/useIntersectionObserver.cjs 967 B
packages/react-simplikit/dist/hooks/useIntersectionObserver/useIntersectionObserver.mjs 939 B
packages/react-simplikit/dist/hooks/useInterval/useInterval.cjs 837 B
packages/react-simplikit/dist/hooks/useInterval/useInterval.mjs 811 B
packages/react-simplikit/dist/hooks/useIsClient/useIsClient.cjs 585 B
packages/react-simplikit/dist/hooks/useIsClient/useIsClient.mjs 574 B
packages/react-simplikit/dist/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.cjs 577 B
packages/react-simplikit/dist/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mjs 575 B
packages/react-simplikit/dist/hooks/useList/useList.cjs 850 B
packages/react-simplikit/dist/hooks/useList/useList.mjs 825 B
packages/react-simplikit/dist/hooks/useLoading/useLoading.cjs 919 B
packages/react-simplikit/dist/hooks/useLoading/useLoading.mjs 909 B
packages/react-simplikit/dist/hooks/useLongPress/useLongPress.cjs 1.68 kB
packages/react-simplikit/dist/hooks/useLongPress/useLongPress.mjs 1.64 kB
packages/react-simplikit/dist/hooks/useMap/useMap.cjs 730 B
packages/react-simplikit/dist/hooks/useMap/useMap.mjs 713 B
packages/react-simplikit/dist/hooks/useOutsideClickEffect/useOutsideClickEffect.cjs 789 B
packages/react-simplikit/dist/hooks/useOutsideClickEffect/useOutsideClickEffect.mjs 758 B
packages/react-simplikit/dist/hooks/usePreservedCallback/usePreservedCallback.cjs 691 B
packages/react-simplikit/dist/hooks/usePreservedCallback/usePreservedCallback.mjs 675 B
packages/react-simplikit/dist/hooks/usePreservedReference/usePreservedReference.cjs 805 B
packages/react-simplikit/dist/hooks/usePreservedReference/usePreservedReference.mjs 789 B
packages/react-simplikit/dist/hooks/usePrevious/usePrevious.cjs 643 B
packages/react-simplikit/dist/hooks/usePrevious/usePrevious.mjs 635 B
packages/react-simplikit/dist/hooks/useRefEffect/useRefEffect.cjs 780 B
packages/react-simplikit/dist/hooks/useRefEffect/useRefEffect.mjs 753 B
packages/react-simplikit/dist/hooks/useSet/useSet.cjs 822 B
packages/react-simplikit/dist/hooks/useSet/useSet.mjs 791 B
packages/react-simplikit/dist/hooks/useStorageState/storage.cjs 543 B
packages/react-simplikit/dist/hooks/useStorageState/storage.mjs 527 B
packages/react-simplikit/dist/hooks/useStorageState/useStorageState.cjs 966 B
packages/react-simplikit/dist/hooks/useStorageState/useStorageState.mjs 957 B
packages/react-simplikit/dist/hooks/useThrottle/throttle.cjs 306 B
packages/react-simplikit/dist/hooks/useThrottle/throttle.mjs 298 B
packages/react-simplikit/dist/hooks/useThrottle/useThrottle.cjs 864 B
packages/react-simplikit/dist/hooks/useThrottle/useThrottle.mjs 838 B
packages/react-simplikit/dist/hooks/useTimeout/useTimeout.cjs 627 B
packages/react-simplikit/dist/hooks/useTimeout/useTimeout.mjs 600 B
packages/react-simplikit/dist/hooks/useToggle/useToggle.cjs 524 B
packages/react-simplikit/dist/hooks/useToggle/useToggle.mjs 506 B
packages/react-simplikit/dist/hooks/useVisibilityEvent/useVisibilityEvent.cjs 692 B
packages/react-simplikit/dist/hooks/useVisibilityEvent/useVisibilityEvent.mjs 674 B
packages/react-simplikit/dist/index.cjs 1.37 kB
packages/react-simplikit/dist/index.mjs 997 B
packages/react-simplikit/dist/mobile/hooks/useAvoidKeyboard/useAvoidKeyboard.cjs 990 B
packages/react-simplikit/dist/mobile/hooks/useAvoidKeyboard/useAvoidKeyboard.mjs 968 B
packages/react-simplikit/dist/mobile/hooks/useBodyScrollLock/useBodyScrollLock.cjs 551 B
packages/react-simplikit/dist/mobile/hooks/useBodyScrollLock/useBodyScrollLock.mjs 527 B
packages/react-simplikit/dist/mobile/hooks/useKeyboardHeight/useKeyboardHeight.cjs 690 B
packages/react-simplikit/dist/mobile/hooks/useKeyboardHeight/useKeyboardHeight.mjs 671 B
packages/react-simplikit/dist/mobile/hooks/useNetworkStatus/useNetworkStatus.cjs 1.2 kB
packages/react-simplikit/dist/mobile/hooks/useNetworkStatus/useNetworkStatus.mjs 1.19 kB
packages/react-simplikit/dist/mobile/hooks/usePageVisibility/usePageVisibility.cjs 936 B
packages/react-simplikit/dist/mobile/hooks/usePageVisibility/usePageVisibility.mjs 917 B
packages/react-simplikit/dist/mobile/hooks/useSafeAreaInset/useSafeAreaInset.cjs 947 B
packages/react-simplikit/dist/mobile/hooks/useSafeAreaInset/useSafeAreaInset.mjs 932 B
packages/react-simplikit/dist/mobile/hooks/useScrollDirection/useScrollDirection.cjs 960 B
packages/react-simplikit/dist/mobile/hooks/useScrollDirection/useScrollDirection.mjs 949 B
packages/react-simplikit/dist/mobile/hooks/useVisualViewport/useVisualViewport.cjs 1.24 kB
packages/react-simplikit/dist/mobile/hooks/useVisualViewport/useVisualViewport.mjs 1.22 kB
packages/react-simplikit/dist/mobile/utils/disableBodyScrollLock/disableBodyScrollLock.cjs 681 B
packages/react-simplikit/dist/mobile/utils/disableBodyScrollLock/disableBodyScrollLock.mjs 673 B
packages/react-simplikit/dist/mobile/utils/enableBodyScrollLock/enableBodyScrollLock.cjs 677 B
packages/react-simplikit/dist/mobile/utils/enableBodyScrollLock/enableBodyScrollLock.mjs 666 B
packages/react-simplikit/dist/mobile/utils/getKeyboardHeight/getKeyboardHeight.cjs 613 B
packages/react-simplikit/dist/mobile/utils/getKeyboardHeight/getKeyboardHeight.mjs 607 B
packages/react-simplikit/dist/mobile/utils/getSafeAreaInset/getSafeAreaInset.cjs 907 B
packages/react-simplikit/dist/mobile/utils/getSafeAreaInset/getSafeAreaInset.mjs 898 B
packages/react-simplikit/dist/mobile/utils/isAndroid/isAndroid.cjs 547 B
packages/react-simplikit/dist/mobile/utils/isAndroid/isAndroid.mjs 538 B
packages/react-simplikit/dist/mobile/utils/isIOS/isIOS.cjs 758 B
packages/react-simplikit/dist/mobile/utils/isIOS/isIOS.mjs 751 B
packages/react-simplikit/dist/mobile/utils/isKeyboardVisible/isKeyboardVisible.cjs 434 B
packages/react-simplikit/dist/mobile/utils/isKeyboardVisible/isKeyboardVisible.mjs 423 B
packages/react-simplikit/dist/mobile/utils/isServer/isServer.cjs 386 B
packages/react-simplikit/dist/mobile/utils/isServer/isServer.mjs 384 B
packages/react-simplikit/dist/mobile/utils/subscribeKeyboardHeight/subscribeKeyboardHeight.cjs 1.12 kB
packages/react-simplikit/dist/mobile/utils/subscribeKeyboardHeight/subscribeKeyboardHeight.mjs 1.1 kB
packages/react-simplikit/dist/utils/buildContext/buildContext.cjs 809 B
packages/react-simplikit/dist/utils/buildContext/buildContext.mjs 788 B
packages/react-simplikit/dist/utils/mergeProps/mergeProps.cjs 669 B
packages/react-simplikit/dist/utils/mergeProps/mergeProps.mjs 668 B
packages/react-simplikit/dist/utils/mergeRefs/mergeRefs.cjs 614 B
packages/react-simplikit/dist/utils/mergeRefs/mergeRefs.mjs 612 B

compressed-size-action

…rning to the last forwarded value

Same ordering defect as useDebouncedCallback. The default leading edge
forwards the intermediate value immediately and masked it; with
edges: ['trailing'] the pending call survived the dedupe early return.
@hyesungoh hyesungoh changed the title fix(useDebouncedCallback): cancel the pending call when returning to the last forwarded value fix(useDebouncedCallback, useThrottledCallback): cancel the pending call when returning to the last forwarded value Aug 28, 2026
@hyesungoh
hyesungoh merged commit a4251a3 into main Aug 28, 2026
18 checks passed
@hyesungoh
hyesungoh deleted the fix/debounced-callback-stale-pending branch August 28, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants