fix(useDevicePixelRatio): remove the listener the last resubscribe added - #231
Merged
childrentime merged 1 commit intoSep 22, 2026
Merged
Conversation
`observe` registers a `change` listener with `{ once: true }` and returns
a cleanup for it, but `handleChange` calls `observe` again and throws the
new cleanup away. The effect only ever holds the cleanup for the first
subscription, which `{ once: true }` has already discarded by then, so
after any ratio change a listener outlives the component.
Keeping the current cleanup in a ref lets the effect release whichever
subscription is live at unmount.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
Good catch — merged. Thanks! |
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.
Description
observesubscribes with{ once: true }and returns a cleanup for that subscription. ButhandleChangecallsobserveagain, and the cleanup that inner call returns is discarded:The effect therefore holds the cleanup for the first subscription only — and
{ once: true }has already removed that listener by the time anything changes. Every subscription after the first outlives the component.Counting live listeners on a
matchMediafake that honoursonce:The leaked listener keeps the hook's closure, and hence the component, reachable; it also calls
setPixelRatioon an unmounted component on the next change.Keeping the current cleanup in a ref lets the effect release whichever subscription is live when it runs.
Type of Change
Checklist
The hook had no spec; this adds three. The shared
createMockMediaMatcheronly implements the legacyaddListener/removeListenerpair, so the spec brings its own fake that records listeners and honours{ once: true }— without that the counts are wrong in the flattering direction, which is how I first mis-measured this.Mutation check: reverting only
index.tsfails exactly the unmount case and leaves the two behavioural ones green.jestis 430 passing,eslintclean. Nointerface.tschange, so no docs regeneration.Not touched:
if (!window) returnat the top cannot work as an SSR guard, because referencing an undeclaredwindowthrowsReferenceErrorrather than returning falsy. It is harmless here since the call only happens inside an effect, but it is not doing what it looks like it does.The diff was written by Claude Opus 5 in Claude Code under my direction; I have not read it line by line myself yet, so that box stays unticked.
🤖 Generated with Claude Code