Skip to content

feat(dom): add useThrottledCallback hook + fix useSyncExternalStore stale closure#36343

Open
srpatcha wants to merge 3 commits into
react:mainfrom
srpatcha:chore/add-gitattributes
Open

feat(dom): add useThrottledCallback hook + fix useSyncExternalStore stale closure#36343
srpatcha wants to merge 3 commits into
react:mainfrom
srpatcha:chore/add-gitattributes

Conversation

@srpatcha

@srpatcha srpatcha commented Apr 25, 2026

Copy link
Copy Markdown

Summary

Two React improvements:

  1. useThrottledCallback hook (new) — packages/react-dom/src/hooks/useThrottledCallback.js

    • Configurable delay with leading/trailing edge options
    • cancel() and flush() API for pending invocations
    • Pending-state tracking
    • Proper cleanup on unmount (timer cancellation)
    • Uses refs to always invoke latest callback without re-creating throttled fn
  2. useSyncExternalStore stale-closure fixpackages/react-reconciler/src/ReactFiberHooks.js

    • Adds isSubscribed guard to handleStoreChange to prevent the callback from firing after the component has conceptually unmounted but before the passive-effect cleanup runs
    • Cleanup sets isSubscribed=false before calling unsubscribe, closing the timing window where handleStoreChange could operate on a stale fiber reference
  3. Jest testspackages/react-dom/src/__tests__/useThrottledCallback-test.js (331 lines)

    • Leading edge immediate invocation
    • Throttling within delay window
    • Leading-only and trailing-only modes
    • cancel() prevents pending trailing invocations
    • flush() triggers immediate invocation
    • Latest callback ref is used without re-creating throttled fn
    • Timer cleanup on unmount prevents stale invocations

Scope

Was previously titled "fix: handle null-prototype objects in typeName helper" — that commit's actual code change was lost during an earlier squash, leaving only file-permission changes that conflicted with upstream deletions. Dropped the empty-content commit and re-scoped this PR to the 3 substantive hooks/test changes.

Files

File Δ
packages/react-dom/src/hooks/useThrottledCallback.js +228 / 0 (new)
packages/react-dom/src/__tests__/useThrottledCallback-test.js +331 / 0 (new)
packages/react-reconciler/src/ReactFiberHooks.js +14 / -1
Total +573 / -1

@meta-cla meta-cla Bot added the CLA Signed label Apr 25, 2026
@srpatcha srpatcha force-pushed the chore/add-gitattributes branch from 6ed3fef to 60106f2 Compare May 16, 2026 16:29
srpatcha added 3 commits May 31, 2026 18:00
Add throttled callback hook with:
- Configurable delay with leading/trailing edge options
- Cancel and flush API for pending invocations
- Pending state tracking
- Proper cleanup on unmount (timer cancellation)
- Uses refs to always invoke latest callback without re-creating throttled fn
- TypeScript-compatible JSDoc annotations

Signed-off-by: Srikanth Patchava <spatchava@meta.com>
Add isSubscribed guard to handleStoreChange in subscribeToStore to
prevent the callback from firing after the component has conceptually
unmounted but before the passive effect cleanup runs. The cleanup now
sets isSubscribed=false before calling unsubscribe, closing the timing
window where handleStoreChange could operate on a stale fiber reference.

Signed-off-by: Srikanth Patchava <spatchava@meta.com>
Test coverage includes:
- Leading edge immediate invocation
- Throttling within delay window
- Leading-only and trailing-only modes
- Cancel API prevents pending trailing invocations
- Flush API triggers immediate invocation
- Latest callback ref is used without re-creating throttled fn
- Timer cleanup on unmount prevents stale invocations

Signed-off-by: Srikanth Patchava <spatchava@meta.com>
@srpatcha srpatcha force-pushed the chore/add-gitattributes branch from 60106f2 to 171ed8c Compare June 1, 2026 01:00
@srpatcha srpatcha changed the title fix: handle null-prototype objects in typeName helper feat(dom): add useThrottledCallback hook + fix useSyncExternalStore stale closure Jun 1, 2026
@srpatcha

srpatcha commented Jun 1, 2026

Copy link
Copy Markdown
Author

Cleaned up + rebased onto current main (commit 171ed8c):

  • ✅ Rebased onto current main (was BEHIND with merge conflict)
  • ✅ Dropped the empty-content a8dd8395 commit (its actual typeName code was lost in an earlier squash, leaving only file-mode changes that conflicted with upstream's release-script deletions)
  • ✅ Re-scoped PR to the 3 substantive commits: useThrottledCallback hook + useSyncExternalStore stale-closure fix + Jest tests
  • ✅ Updated title and body to reflect actual scope

Ready for re-review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants