fix(sticky-headers): guard binary search against unmeasured layouts - #2507
Open
priyanshu-cashbook wants to merge 2 commits into
Open
priyanshu-cashbook wants to merge 2 commits into
priyanshu-cashbook wants to merge 2 commits into
Conversation
`compute()` bails out on `getDataLength()`, which is `props.data.length` and grows during render, then reads positions through `getLayout()`, which is backed by `layoutManager.layouts` and only grows in `modifyChildrenLayout()` once the children have laid out. A scroll event arriving between those two commits passes the `lengthInvalid` check with a sticky index the layout table has not reached, and `getLayout` throws "index out of bounds, not enough layouts" out of `onScroll`, taking the tree down. Read through `tryGetLayout()` — as the two reads directly below already do — and treat a missing layout as below the viewport. The newly appended tail is what goes unmeasured, so that is also the correct answer, not just a safe one: the last measured header stays selected instead of one that may not be on screen yet.
Author
|
I have signed the CLA |
This branch has not been deployed
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
StickyHeaders.compute()throwsindex out of bounds, not enough layoutsout of the scroll handler, taking the tree down. Seen in production on 2.0.2 across iOS and Android — 45 users in four days on a single screen, all of them a paginating list withstickyHeaderIndices.The window
compute()bails out on a data-prop length but then reads positions from the layout table:getDataLength()isprops.data.length, set byupdateProps()during render.getLayout()throws onindex >= this.layouts.length, andlayoutsonly grows inmodifyLayout(), reached frommodifyChildrenLayout()after the children have laid out.So when a page is appended,
dataandstickyHeaderIndicesupdate together in one render andlengthInvalidis satisfied for the new trailing header, butlayoutsis still the previous length for at least one more commit.onScrollHandlercallsstickyHeaderRef.current?.reportScrollEvent()→compute(), and any scroll event landing in that gap throws.It needs a list that both paginates and sets
stickyHeaderIndices, which is why it is not constant. Shrinks are safe (the table is truncated on the first line ofmodifyLayout, so it is never shorter than the data); only growth exposes it.Fix
Read through
tryGetLayout()and treat a missing layout as below the viewport.The two reads immediately below the binary search already do exactly this, and #2460 assumes this file is already safe on that basis (
"as StickyHeaders, the measurement effect and the public getLayout ref already do") — thegetYcallback is the one read in here that was still unguarded. That PR coversViewHolderCollectionandvalidateItemSizeon the shrink path, so the two are complementary rather than overlapping; this one needs no coordination with it.Number.MAX_SAFE_INTEGERis also the correct answer and not just a non-throwing one: what goes unmeasured is the freshly appended tail, which really is below everything measured, so the last measured header stays selected instead of one that may not be on screen yet. The guard is inert whenever the index is valid, so no list that is not already in this state changes behaviour.Reviewers' hat-rack 🎩
src/__tests__/StickyHeaders.test.tsx— two cases added, driving the real component through a manager whosegetLayoutthrows pastmeasuredCountand whosetryGetLayoutreturnsundefined, matchingRVLayoutManager/RecyclerViewManager. Both fail onmainwith the exact production message and pass with the fix; the 19 existing cases in the file are untouched by it.compute()early instead was the other option, butlengthInvalidis derived from data rather than layouts, so nothing would re-runcompute()once the layouts caught up and the header could stay stale until the next scroll event.yarn test --forceExit189 passed / 14 suites,yarn type-checkandyarn lintclean.Related: #2440, #2291, #2460.