Skip to content

fix: flush visual order after native SortableContainer reorder - #304

Open
ottob wants to merge 1 commit into
nuclearpasta:mainfrom
ottob:fix/flush-visual-order-on-native-reorder
Open

ottob wants to merge 1 commit into
nuclearpasta:mainfrom
ottob:fix/flush-visual-order-on-native-reorder

Conversation

@ottob

@ottob ottob commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Ensures sortable._internal.flushVisualOrder() is also called on native (iOS / Android) after a standalone SortableContainer (DraxList) reorder completes.
  • Previously, finalizeDrag only called flushVisualOrder() when boardContext was present (setTimeout(..., 300)) or when Platform.OS === 'web'.
  • On native standalone lists, stableData was never synced to rawData after a reorder, leaving items permanently shifted via translateY/translateX transforms rather than updating their actual layout order (which breaks native touch hit-testing on shifted child controls such as DraxHandle or buttons).

@vercel

vercel Bot commented Oct 30, 2026

Copy link
Copy Markdown

@ottob is attempting to deploy a commit to the Nuclear Pasta Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs-site Ready Ready Preview Oct 31, 2026 3:10pm UTC
drax-example Ready Ready Preview Oct 31, 2026 3:10pm UTC

@LunatiqueCoder LunatiqueCoder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 posted by [drax] Reviewer, Ovidiu's review bot

thanks ottob, this fixes a real bug, and it's worse than the description says. one thing to sort out before merging.

repro on main (iOS): after a native drop in a standalone SortableContainer, stableData never updates, because the rawData effect sees the echoed order and returns early. I saw:

  • renderItem indexes and the built-in "Item N of M" a11y label stay stale (the top row reads "Item 2 of 6")
  • a later parent update that keeps the same key order is silently dropped (the parent has D1, the list keeps showing D0)
  • on the next drag, onReorder hands back the old item objects, so that edit is lost

with this PR all three are fixed: indexes and labels are right, the edit shows up immediately, and it survives the next reorder. worth adding the dropped-update and data-loss cases to the description, since they're a stronger reason than hit-testing.

the blocker: the setTimeout(0) reopens the race the web comment warns about. if a new drag starts after onReorder and before the flush fires, flushVisualOrder swaps stableData and clears committedOrderRef under the active drag, so pendingOrderRef points into the old array. I reproduced it: expected F E C D B A, got F E D B C A. the window is small, but main didn't have it on native. could native use the same synchronous flush as web? React batches the parent's setData with setStableData there. or at least skip or re-defer the flush while a drag is active.

optional hardening: if the parent doesn't store onReorder data, the flush sets stableData to the same reference, so the shift-clearing effect never runs while the committed refs do get cleared. the next drag then jumps. web already behaves this way; this extends it to native.

how I tested: yarn typecheck and yarn lint pass, and there are no unit tests yet. touch injection wasn't working on the simulator I had, so instead of finger drags I drove the real finalizeDrag path (the function this PR changes) from the debugger, on a temporary screen with a FlatList SortableContainer. the gesture, hover and animation code wasn't exercised, so a quick real-finger pass (two fast drags in a row, plus the cross-list screen) would still be good.

Comment thread src/SortableContainer.tsx
// races when the user grabbed another item before it fired.
sortable._internal.flushVisualOrder();
} else {
setTimeout(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 posted by [drax] Reviewer, Ovidiu's review bot

deferring with setTimeout(0) reopens the race described in the web comment above: a drag that starts after onReorder but before this fires gets stableData swapped and committedOrderRef cleared underneath it, and the drop scrambles the order (reproduced: expected F E C D B A, got F E D B C A). could native share the synchronous web path, or skip/re-defer while draggedItem.value !== -1?

Comment thread src/SortableContainer.tsx
sortable._internal.flushVisualOrder();
} else {
setTimeout(() => {
sortable._internal.flushVisualOrder();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 posted by [drax] Reviewer, Ovidiu's review bot

non-blocking: if the parent ignores onReorder, setStableData gets the same reference, so React bails out and the shift-clearing layout effect never runs, but the committed refs are cleared. the view keeps the dragged order while the hook thinks it's identity, and the next drag jumps. maybe guard for that in flushVisualOrder (useSortableList.ts:557).

This branch was successfully deployed

2 active deployments
Preview – drax-example — 34371b79 Deployed Oct 31, 2026 by vercel[bot]
Preview – docs-site — 34371b79 Deployed Oct 31, 2026 by vercel[bot]
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.

2 participants