Repository navigation
fix: stop leaking scroll listeners under React StrictMode - #2894
Merged
n0rahh merged 2 commits intoNov 10, 2026
Merged
Conversation
AppStore bound its window scroll listener and its menu subscriptions in the constructor. StoreBuilder builds the store inside useMemo, which StrictMode runs twice, so the store that was never rendered kept its listener forever. StrictMode's simulated unmount/remount of <Redoc> also disposed the live store, and onDidMount never bound it again. Bind the scroll listener and the menu subscriptions in onDidMount instead, make binding idempotent, and let a disposed store be mounted again. <Redoc> now also disposes the previous store and mounts the new one when its store prop changes, which StoreBuilder does whenever the options object changes. Fixes Redocly#2842
hasnnbhan1-beep
approved these changes
Nov 10, 2026
n0rahh
reviewed
Nov 10, 2026
n0rahh
left a comment
Contributor
There was a problem hiding this comment.
Hi, thanks for the PR! Here is one small finding:
n0rahh
approved these changes
Nov 10, 2026
n0rahh
left a comment
Contributor
There was a problem hiding this comment.
LGTM, thanks for the contribution!
For the follow-up you mentioned (moving the worker cleanup to StoreBuilder, which owns the store), it would be great to cover both cases:
- a StrictMode dev remount of
<Redoc>terminates the worker of the store that is still shown, so search stops working; - a store that
StoreBuilderreplaces (e.g. inlineoptions={{ ... }}re-created on every parent render) now only getsdetach(), so its search worker is never terminated and each such re-render leaves one worker thread behind (same as onmaintoday).
2 of 3 tasks
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.
What/Why/How?
Fixes #2842 (v2:
RedocStandaloneunder React Strict Mode).AppStoreattached itswindowscroll listener (ScrollService) and its menu subscriptions in the constructor.StoreBuilderbuilds the store inuseMemo, which Strict Mode runs twice, so the store that is never rendered keeps its listener after unmount. Strict Mode's simulated unmount/remount of<Redoc>also disposed the live store, andonDidMountnever bound it again, so the mounted docs lost their scroll-spy in development.Changes:
ScrollServiceno longer binds in its constructor.bind()is idempotent anddispose()resets it.MenuStoreno longer subscribes in its constructor.subscribe()is idempotent anddispose()clears the handles.AppStore.onDidMount()binds the scroll listener, subscribes the menu and re-registers the active-item observer, so a disposed store can be mounted again.<Redoc>disposes the previous store and mounts the new one when itsstoreprop changes.StoreBuildercreates a new store whenever theoptionsobject changes, e.g. an inlineoptions={{ ... }}. Before this change the old store's listener stayed attached.Not covered: a store disposed by the Strict Mode remount still has its search worker terminated. That is unchanged here.
The PR targets
v2.x, becausemain(v3) no longer hasStoreBuilder/AppStore.Reference
Fixes #2842
Tests
New
src/components/__tests__/Redoc.test.tsxcounts thescrolllisteners onwindow:RedocStandaloneinStrictMode, after unmountRedocinStrictMode, while mountedRedocre-rendered with a new storenpx jest: 28 suites, 279 tests passnpm run ts-check,eslintandprettier --checkon the changed files,npm run bundle,npm run license-check: passCheck yourself