Versions: @legendapp/list 3.2.0, React Native 0.81.5 (Expo), iOS.
Setup: chat app for coding-agent transcripts (tool-heavy sessions; row heights measured 18–322px). List config: recycleItems, getItemType (3 kinds), estimatedItemSize={140}, and the container is flipped with scaleY(-1) so contentOffset 0 is the visual bottom (we know the README recommends alignItemsAtEnd + maintainScrollAtEnd instead of inverting; we use the flip on native because appends are flash-free when the anchor is offset 0 — happy to expand on that trade-off).
Symptom
While the list is anchored at the end (offset 0 in flipped space) and the data changes, internal re-layout from size-estimate corrections physically moves the viewport:
- Sending a message into an estimate-heavy transcript:
contentOffset.y traced 0 → 769 → 0 across consecutive scroll ticks — the transcript visibly teleports up and back.
- A second jump lands ~2s later as streamed rows measure and re-price.
- On a cold open of a 1,056-row transcript, content height at first paint was 27% under the fully-measured value (109,283 → 150,083 after every row measured), so the first interactions happen while large corrections are still flowing through.
Why we couldn't fix it with the existing API
maintainVisibleContentPosition: in flipped space it anchors the first visible item in internal coordinates, which is the visually OLD content — new appended content shoves the viewport instead of growing away from it. Tested and rejected.
- Per-type estimates:
getItemType feeds the internal per-type average learning, but the averages learn from the first-visible rows — in a chat that's the short recent tail. Measured on the 1,056-row transcript: tail-learned averages priced turn rows at ~72px vs a true transcript-wide mean of 155px, which is most of the 27% error above.
- No per-item estimate hook:
estimatedItemSize is a scalar, getFixedItemSize is fixed-size-only (and opts the type out of average learning), overrideItemLayout is span-only. We hold the row content and could price rows accurately (e.g. char counts), but there's no API to supply it.
Our current mitigation is an app-side onScroll clamp: while anchored at the end with no active gesture, any large off-anchor tick is treated as re-layout turbulence and re-landed at offset 0. It works, but it's fighting the library from the outside and has to guess "no gesture" from drag/momentum events.
Request
Either (ideally both) of:
- An explicit at-bottom/followOutput contract — when the list is anchored at its end (within a threshold), suppress or zero out internal scroll adjustments from estimate corrections so the anchor holds, in the spirit of react-virtuoso's
followOutput/atBottomThreshold and CSS scroll anchoring. If it could also hold for externally-flipped usage (anchor = offset 0), chat apps that invert for flash-free appends would benefit directly.
- A per-item size estimate hook — e.g.
getEstimatedItemSize(item, index, type) used only until the row first measures (unlike getFixedItemSize it should not claim the size as known or opt out of average learning). This lets apps shrink the corrections at the source instead of absorbing them.
We saw the 3.3.0 changelog entry about maintainScrollAtEnd re-running pending keep-at-end requests (footer resize + new message) — that looks related for the non-inverted path, but the ask here is the general contract under estimate re-layout.
Happy to build a minimal repro (≈1,000 rows, 50–320px height variance, append + stream while anchored) if that's useful.
Versions:
@legendapp/list3.2.0, React Native 0.81.5 (Expo), iOS.Setup: chat app for coding-agent transcripts (tool-heavy sessions; row heights measured 18–322px). List config:
recycleItems,getItemType(3 kinds),estimatedItemSize={140}, and the container is flipped withscaleY(-1)so contentOffset 0 is the visual bottom (we know the README recommendsalignItemsAtEnd+maintainScrollAtEndinstead of inverting; we use the flip on native because appends are flash-free when the anchor is offset 0 — happy to expand on that trade-off).Symptom
While the list is anchored at the end (offset 0 in flipped space) and the data changes, internal re-layout from size-estimate corrections physically moves the viewport:
contentOffset.ytraced0 → 769 → 0across consecutive scroll ticks — the transcript visibly teleports up and back.Why we couldn't fix it with the existing API
maintainVisibleContentPosition: in flipped space it anchors the first visible item in internal coordinates, which is the visually OLD content — new appended content shoves the viewport instead of growing away from it. Tested and rejected.getItemTypefeeds the internal per-type average learning, but the averages learn from the first-visible rows — in a chat that's the short recent tail. Measured on the 1,056-row transcript: tail-learned averages pricedturnrows at ~72px vs a true transcript-wide mean of 155px, which is most of the 27% error above.estimatedItemSizeis a scalar,getFixedItemSizeis fixed-size-only (and opts the type out of average learning),overrideItemLayoutis span-only. We hold the row content and could price rows accurately (e.g. char counts), but there's no API to supply it.Our current mitigation is an app-side
onScrollclamp: while anchored at the end with no active gesture, any large off-anchor tick is treated as re-layout turbulence and re-landed at offset 0. It works, but it's fighting the library from the outside and has to guess "no gesture" from drag/momentum events.Request
Either (ideally both) of:
followOutput/atBottomThresholdand CSS scroll anchoring. If it could also hold for externally-flipped usage (anchor = offset 0), chat apps that invert for flash-free appends would benefit directly.getEstimatedItemSize(item, index, type)used only until the row first measures (unlikegetFixedItemSizeit should not claim the size as known or opt out of average learning). This lets apps shrink the corrections at the source instead of absorbing them.We saw the 3.3.0 changelog entry about
maintainScrollAtEndre-running pending keep-at-end requests (footer resize + new message) — that looks related for the non-inverted path, but the ask here is the general contract under estimate re-layout.Happy to build a minimal repro (≈1,000 rows, 50–320px height variance, append + stream while anchored) if that's useful.