Skip to content

small scroll jump when load more data on top #531

Description

@ngocan3897

Title

a small scroll jump even with maintainVisibleContentPosition={{ data: true, size: true }}

Environment

  • @legendapp/list: 3.3.2
  • react-native: 0.83.1
  • react: 19.2.0
  • react-native-reanimated: 4.5.3
  • New Architecture (Fabric/TurboModules): enabled (Android newArchEnabled=true; iOS default for RN 0.83)
  • Platform: iOS / Android (please confirm which you saw it on — add here)

Summary

In a chat-style list that starts with content shorter than the viewport (so alignItemsAtEnd is actively padding the top to bottom-align content), prepending older items via onStartReached causes the viewport to visibly shift up by a small amount for a frame or two, even though maintainVisibleContentPosition is configured exactly per the docs' recommended prepend setup.

Repro

Minimal setup (trimmed from a demo screen):

<KeyboardAwareLegendList
  data={messages}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  estimatedItemSize={64}
  onStartReached={handleLoadMore}
  onStartReachedThreshold={0.5}
  maintainVisibleContentPosition   // normalizes to { data: true, size: true }
  alignItemsAtEnd
  initialScrollAtEnd
  maintainScrollAtEnd
/>
  1. Start with a handful of short messages — few enough that total content height < viewport height (so alignItemsAtEnd's internal top spacer is > 0).
  2. Scroll to the top to trigger onStartReached, and prepend N older items to the front of data (simulating a page of older chat history).
  3. Watch the viewport during/immediately after the prepend: it jumps up slightly before settling, instead of staying perfectly anchored on the content the user was already looking at.

Expected

With maintainVisibleContentPosition={{ data: true, size: true }} and onStartReached used together (the pattern the docs recommend for "load older content at the top"), the viewport should not visibly move at all when older items are prepended.

Suspected root cause (traced through the bundled source, not yet confirmed with frame-level profiling)

  • estimatedItemSize is just a hint; each prepended item's real height is only known after its own onLayout fires — these arrive one at a time, not atomically for the whole prepended batch.

  • Each time an item's measured size lands, addTotalSize() runs and immediately calls updateContentMetricsState() (src/core/addTotalSize.ts → src/core/updateContentMetricsState.ts), which recomputes alignItemsAtEndPadding:

    // src/core/updateContentMetricsState.ts
    function getAlignItemsAtEndPadding(ctx) {
      const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && ...
      return shouldPad
        ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx))
        : 0;
    }
  • Because the batch's items measure in over several separate passes rather than one, this top spacer shrinks in several small increments across frames instead of one atomic adjustment at prepend time.

  • maintainVisibleContentPosition's size: true mode relies on native minIndexForVisible: 0 to compensate for content-height changes above the anchor — this appears well-suited to compensating for real data cells resizing, but the alignItemsAtEnd spacer is a synthetic view injected by the list itself, not a tracked data cell, so its incremental shrink doesn't appear to be fully absorbed by the same compensation path, leaving a small residual jump.

  • This would explain why the jump is small (roughly the size of one measurement-correction step) rather than the full height of the prepended batch, and why it only shows up when alignItemsAtEnd's padding is actually non-zero at prepend time (i.e., sparse initial content).

Question for maintainers

  • Is alignItemsAtEndPadding's shrink-on-measure path expected to be covered by maintainVisibleContentPosition, or is this a known gap (similar in spirit to the existing shrink-compensation special-cased for stylePaddingTop in src/utils/setPaddingTop.ts, which isn't applied to alignItemsAtEndPadding)?
  • If it's a gap, would applying the same kind of one-frame totalSize hold-and-release used in setPaddingTop to alignItemsAtEndPadding shrink events be the right fix?

Happy to provide a runnable repro snack/repo if useful.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions