Bug Description
SortableItem destructures onItemSnapEnd from sortable._internal at render time (line 129 of SortableItem.tsx), but SortableContainer only sets it via a useLayoutEffect that runs after render (line 217 of SortableContainer.tsx). Since useSortableList creates a new _internal object every render with onItemSnapEnd: undefined (line 1026 of useSortableList.ts), the destructured value is always undefined.
This means finalizeDrag is never called after the snap animation completes, so onReorder never fires and items snap back to their original positions.
Steps to Reproduce
- Set up a
SortableContainer + SortableItem + useSortableList with a FlatList or FlashList inside a SortableBoardContainer (kanban layout)
- Drag an item to a new position — items shift correctly during drag, drop indicator appears
- Release the item
- Expected: item stays at the new position,
onReorder callback fires
- Actual: item snaps back to its original position,
onReorder never fires
Root Cause
In SortableItem.tsx:
// Line 129 — destructured at render time, gets `undefined`
const { ..., onItemSnapEnd, ... } = sortable._internal;
// Line 223-225 — closure captures `undefined`, so finalizeDrag is never called
onSnapEnd={(snapData) => {
onItemSnapEnd?.(); // always undefined!
draxViewProps.onSnapEnd?.(snapData);
}}
In SortableContainer.tsx:
// Line 216-218 — sets onItemSnapEnd AFTER render, on a new _internal object each time
useLayoutEffect(() => {
sortable._internal.onItemSnapEnd = finalizeDrag;
}, [sortable._internal, finalizeDrag]);
In useSortableList.ts:
// Line 1026 — new _internal object created every render with undefined
onItemSnapEnd: undefined as (() => void) | undefined,
The type definition even has a comment describing the intended behavior (line 813-814 of types.ts):
/** Called by SortableItem's onSnapEnd to finalize the drag.
* Stored as a ref so the latest finalizeDrag is always called,
* even if SortableItem has a stale _internal reference. */
The intent was correct — onItemSnapEnd should act as a ref-like property read at call time — but the implementation eagerly destructures it.
Fix
One-line change in SortableItem.tsx — read onItemSnapEnd from sortable._internal at call time instead of at destructure time:
onSnapEnd={(snapData) => {
- onItemSnapEnd?.();
+ sortable._internal.onItemSnapEnd?.();
draxViewProps.onSnapEnd?.(snapData);
}}
This ensures the callback reads the finalizeDrag function that SortableContainer set in its useLayoutEffect, rather than the undefined captured during destructuring.
Environment
react-native-drax: 1.1.0
react-native: 0.79
react-native-reanimated: 4.2.2
expo: 55
- Using
FlashList (same behavior expected with FlatList)
- Layout:
SortableBoardContainer > SortableContainer > FlashList > SortableItem (kanban board with multiple columns)
Workaround
We're using a bun patch (patches/react-native-drax@1.1.0.patch) with the fix above. Same patch can be applied via patch-package for npm/yarn users.
Bug Description
SortableItemdestructuresonItemSnapEndfromsortable._internalat render time (line 129 ofSortableItem.tsx), butSortableContaineronly sets it via auseLayoutEffectthat runs after render (line 217 ofSortableContainer.tsx). SinceuseSortableListcreates a new_internalobject every render withonItemSnapEnd: undefined(line 1026 ofuseSortableList.ts), the destructured value is alwaysundefined.This means
finalizeDragis never called after the snap animation completes, soonReordernever fires and items snap back to their original positions.Steps to Reproduce
SortableContainer+SortableItem+useSortableListwith aFlatListorFlashListinside aSortableBoardContainer(kanban layout)onReordercallback firesonReordernever firesRoot Cause
In
SortableItem.tsx:In
SortableContainer.tsx:In
useSortableList.ts:The type definition even has a comment describing the intended behavior (line 813-814 of
types.ts):The intent was correct —
onItemSnapEndshould act as a ref-like property read at call time — but the implementation eagerly destructures it.Fix
One-line change in
SortableItem.tsx— readonItemSnapEndfromsortable._internalat call time instead of at destructure time:onSnapEnd={(snapData) => { - onItemSnapEnd?.(); + sortable._internal.onItemSnapEnd?.(); draxViewProps.onSnapEnd?.(snapData); }}This ensures the callback reads the
finalizeDragfunction thatSortableContainerset in itsuseLayoutEffect, rather than theundefinedcaptured during destructuring.Environment
react-native-drax: 1.1.0react-native: 0.79react-native-reanimated: 4.2.2expo: 55FlashList(same behavior expected withFlatList)SortableBoardContainer>SortableContainer>FlashList>SortableItem(kanban board with multiple columns)Workaround
We're using a bun patch (
patches/react-native-drax@1.1.0.patch) with the fix above. Same patch can be applied viapatch-packagefor npm/yarn users.