Skip to content

itemLayoutAnimation leaves rows permanently misaligned when items reorder around a moving row #513

Description

@m9tdev

Ran into this while building an outliner app with drag 'n' drop. I added custom two finger interactions to make it possible to drag an item while scrolling and opening/closing trees. The item would constantly be moved in the items array to create an empty spot where it would drop. Opening and closing trees underneath the dragged/hovering item would mess up the alignment of items that moved underneath it. Also confirmed using a dev build on my physical iPhone 16 Pro btw, so it's not only on the simulator. Didn't test Android though.

--- Below is output from Claude Opus 5 ---

AnimatedLegendList with itemLayoutAnimation and getFixedItemSize settles some rows off their correct position. The offset does not recover — it stays until the list remounts.

Repro (single self-contained component): https://gist.github.com/m9tdev/e990ed5a450810346e73378cd1b02d67

Setup

package version
@legendapp/list 3.3.3
react-native-reanimated 4.5.0
react-native 0.86.0
react 19.2.3
expo 57.0.8

iPhone 17 Pro simulator, iOS 26.5, macOS 26.3.1. Debug and release builds both show it.

Steps

  1. Render the component from the gist.
  2. Press Run 16 fast toggles (90ms apart).
  3. Do not scroll. Read the guide lines.

Each toggle does two things at once:

  • expands or collapses a parent, so the row list is re-derived and every row arrives as a fresh object;
  • moves one extra >>> floating <<< row to a new index.

The second part is what matters. Rows must reorder around the floating row, not merely shift down as a block. Toggling alone does not trigger the bug — I could not reproduce it until I added the moving row.

Toggles are 90ms apart and the transition is 160ms, so each transition is interrupted mid-flight.

Expected

Every row sits on a guide line. Rows are a fixed 30pt and getFixedItemSize returns 30, so every gap must be 30pt.

Actual

At least one gap comes out ~3pt wrong, and every row below inherits the shift and keeps it. Measured through the accessibility tree (normalized screen fractions, 30pt = 0.0343):

child 1             0.261
>>> floating <<<    0.298   Δ 0.0372   ← 3.5pt long
child 2             0.330   Δ 0.0320   ← 3pt short
child 3             0.364   Δ 0.0343   ok
...
child 13            0.707   Δ 0.0343   ok
parent 1            0.738   Δ 0.0310   ← 3pt short
parent 2            0.772   Δ 0.0343   ok

Nothing re-settles it. Further toggles keep the shift, and can add more. Only remounting the list clears it. Which rows land wrong varies per run; every run so far has had at least one.

Controls

change result
LAYOUT_ANIMATION = false aligned, all gaps 0.0343
RECYCLE_ITEMS = false still drifts — not a factor

The control run is the same 16 toggles with the same data churn, only without itemLayoutAnimation. It ends perfectly uniform end to end.

Why it matters

This came out of a drag-and-drop outliner. A dragged row is spliced into the list at a moving index while the user opens and closes subtrees underneath it — exactly the pattern above. After a few seconds of dragging the list is visibly crooked and stays that way. I had to drop itemLayoutAnimation entirely.

Guess, not a diagnosis

Legend List positions each row through a React top style applied at commit time. itemLayoutAnimation positions it through a Reanimated animated style applied on the UI thread. The two can never change on the same frame. When a row's top changes while its layout transition is still in flight, the two sources appear to disagree about where the row started, and the difference is kept.

That is inferred from behaviour only — I have not read the internals. Treat it as a hint, not a diagnosis.

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