Skip to content

Support virtualized lists in Reorder#3636

Open
mattgperry wants to merge 3 commits intomainfrom
worktree-fix-issue-3061
Open

Support virtualized lists in Reorder#3636
mattgperry wants to merge 3 commits intomainfrom
worktree-fix-issue-3061

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Bug: Reorder.Group's updateOrder callback only returned measured/rendered items to onReorder, dropping any items not currently in the DOM (e.g. offscreen items in a virtualized list)
  • Cause: updateOrder mapped the internal order array (which only contains measured items) back to values and filtered against the values prop, discarding unmeasured items entirely
  • Fix: Instead of returning only measured items, detect which two values checkReorder swapped and apply that same swap to the full values array. This preserves all unmeasured items in their correct positions

Fixes #3061

Test plan

  • Unit test: verifies that when only a subset of items are registered (simulating virtualization), onReorder returns the full values array with the swap applied
  • All 770 existing tests pass
  • Build succeeds

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR fixes a long-standing bug (#3061) where Reorder.Group's onReorder callback only returned the measured (rendered) items — silently dropping any items that were outside the viewport in a virtualized list.

What changed:

  • Group.tsxupdateOrder no longer maps the internal order array back to values and filters against values. Instead it detects the single pairwise swap that checkReorder produced (by finding the first position where order[i] and newOrder[i] differ), then applies that same swap directly to a copy of the full values prop. This preserves every item, measured or not.
  • index.test.tsx — A new unit test renders a Reorder.Group with 5 values but only registers 3 of them (simulating offscreen items in a virtualized list), triggers a drag past the threshold, and asserts that onReorder returns all 5 values with the correct swap applied.

Key considerations:

  • The swap-detection logic is correct given that checkReorder always delegates to moveItem(order, index, index ± 1), which for adjacent indices is always a simple pairwise swap. The approach would need revisiting if checkReorder were ever extended to support multi-item moves.
  • values.indexOf uses reference equality, which is consistent with the rest of the Reorder API but means duplicate primitive values or re-created object references would not behave correctly — this is a pre-existing limitation, not introduced here.
  • There is a minor robustness gap: if indexOf returns -1 (e.g., a registered item whose value has already been removed from values), the swap silently becomes a no-op rather than throwing or logging a warning.

Confidence Score: 4/5

  • This PR is safe to merge; the fix is logically correct and well-tested, with one minor robustness gap around missing values.
  • The swap-detection algorithm is provably correct given checkReorder's adjacent-only move behaviour (verified by reading moveItem). The new unit test exercises the exact virtualized-list scenario described in the bug. The only concern — a silent no-op when indexOf returns -1 — is a pre-existing edge case that is unlikely to surface in practice and does not regress existing behaviour.
  • The swap logic in packages/framer-motion/src/components/Reorder/Group.tsx lines 129–133 should be reviewed for the silent indexOf === -1 edge case.

Important Files Changed

Filename Overview
packages/framer-motion/src/components/Reorder/Group.tsx Core fix: replaces the old map+filter approach (which dropped unmeasured items) with a swap-detection loop that finds the two values exchanged by checkReorder and applies that same swap to the full values array. Logic is correct for the current adjacent-only swap behavior of checkReorder/moveItem, but silently no-ops if a registered item's value is missing from values.
packages/framer-motion/src/components/Reorder/tests/index.test.tsx New test directly exercises the virtualized-list scenario by registering only a subset of items (2, 3, 4 out of [1,2,3,4,5]) and asserting that onReorder returns the full array with the correct swap applied ([1,3,2,4,5]).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["updateOrder called"] --> B{isReordering?}
    B -- yes --> Z[return early]
    B -- no --> C["checkReorder - order, item, offset, velocity"]
    C --> D{newOrder !== order?}
    D -- no change --> Z
    D -- swap detected --> E["isReordering = true"]
    E --> F["newValues = spread of full values prop"]
    F --> G["Loop over newOrder positions"]
    G --> H{Values differ at position i?}
    H -- no --> G
    H -- yes --> I["Find index a of old value in values\nFind index b of new value in values"]
    I --> J["Swap newValues at a and b"]
    J --> K[break]
    K --> L["onReorder called with full newValues array"]

    style L fill:#d4edda,stroke:#28a745
    style Z fill:#f8d7da,stroke:#dc3545
Loading

Last reviewed commit: c3c3c7f

Comment on lines +129 to +133
const a = values.indexOf(order[i].value)
const b = values.indexOf(newOrder[i].value)
newValues[a] = newOrder[i].value
newValues[b] = order[i].value
break
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent no-op when a registered value is absent from values

If either order[i].value or newOrder[i].value is not present in values, indexOf returns -1 and newValues[-1] = … silently sets a non-indexed property on the array instead of throwing. The result is that onReorder is called with the array effectively unchanged — the intended swap never happens.

This scenario arises when a currently-rendered/registered item's value has already been removed from values (e.g. the user removed a row while it was being dragged). It was not easily observable with the old code either, but the new path makes it worth guarding explicitly:

const a = values.indexOf(order[i].value)
const b = values.indexOf(newOrder[i].value)
if (a !== -1 && b !== -1) {
    newValues[a] = newOrder[i].value
    newValues[b] = order[i].value
}
break

mattgperry and others added 2 commits March 13, 2026 09:44
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support virtualized lists when using Reorder

1 participant