Skip to content

waitForInitialLayout not working as still see visible flash/jump on initial render (with initialScrollAtEnd) #371

@Tom-Standen

Description

@Tom-Standen

Disclaimer

We're currently using a JS-level workaround (wrapping in an Animated.View with delayed opacity) rather than the proposed native fix. The investigation and fix were proposed by Claude Opus-4.5. I'm not familiar with the legend-list internals, so take with a pinch of salt but sharing in case it's helpful for community.

Description

When using initialScrollAtEnd={true} with waitForInitialLayout={true}, there's still a visible flash where the list appears at a nearly-correct scroll position, then jumps slightly as anchor corrections are applied.

Example

Screen.Recording.2025-12-11.at.16.03.36.mov

Root Cause Analysis

After tracing through the code, the issue appears to be in ensureInitialAnchor.ts. Currently, finishScrollTo is scheduled via requestAnimationFrame after every adjustment:

// Line 72-74 in ensureInitialAnchor.ts
requestAdjust(ctx, delta);

requestAnimationFrame(() => finishScrollTo(ctx));

finishScrollTo sets didInitialScroll = true, which (combined with didContainersLayout) triggers readyToRender = true in setInitialRenderState. This controls the list's opacity via waitForInitialLayout.

The apparent problem: readyToRender becomes true immediately after the first adjustment is requested, but before it's visually applied. The user sees the list at the pre-correction position, then it jumps.

The anchor settling logic (tolerance check, max attempts, delta-not-improving) correctly tracks when corrections are done, but finishScrollTo is never called in those exit paths - only after requestAdjust.

Potential Fix (Untested)

Move finishScrollTo calls to when the anchor is actually done, rather than after each adjustment:

// When settled (delta <= tolerance for 2 ticks)
if (settledTicks >= INITIAL_ANCHOR_SETTLED_TICKS) {
    state.initialAnchor = undefined;
+   finishScrollTo(ctx);
}

// When max attempts reached
if ((anchor.attempts ?? 0) >= INITIAL_ANCHOR_MAX_ATTEMPTS) {
    state.initialAnchor = undefined;
+   finishScrollTo(ctx);
    return;
}

// When delta not improving  
if (lastDelta !== undefined && Math.abs(delta) >= Math.abs(lastDelta)) {
    state.initialAnchor = undefined;
+   finishScrollTo(ctx);
    return;
}

// Remove premature call after adjustment
requestAdjust(ctx, delta);
-
-requestAnimationFrame(() => finishScrollTo(ctx));

Environment

  • legend-list version: 3.0.0-beta.8
  • Platform: Android simulator and device
  • React Native: 0.76.x

Current Workaround

We're using a JS-level workaround - wrapping the list in an Animated.View with controlled opacity, using the onLoad callback with a small delay (~50ms) before showing:

const [isReady, setIsReady] = useState(false);

const handleLoad = useCallback(() => {
  setTimeout(() => setIsReady(true), 50);
}, []);

<Animated.View style={{ opacity: isReady ? 1 : 0, flex: 1 }}>
  <LegendList 
    onLoad={handleLoad} 
    initialScrollAtEnd={true}
    waitForInitialLayout={true}
    ...
  />
</Animated.View>

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions