Environment
- react-native-drax: 1.1.0
- react-native-reanimated: 4.x
- Platform: web and native
Symptom
When dragging list items rapidly (releasing one drag and starting another quickly), an item disappears from the list. The visual glitch is permanent until the next data refresh.
Root cause — two related races
Race 1: stale hoverReadySV / draggedIdSV between rapid drags
In finalizeDrag (SortableContainer.tsx), the drag state is cleared via a runOnUI worklet:
runOnUI((_shifts) => {
'worklet';
// ...
hoverReadySV.value = false; // ← cleared asynchronously
draggedIdSV.value = ''; // ← cleared asynchronously
// ...
})(finalShifts);
requestAnimationFrame(() => {
onReorder(reorderEvent);
// ...
});
The runOnUI worklet is dispatched before the RAF but executes asynchronously on the UI thread. If the user starts a second drag before the worklet completes:
hoverReadySV.value is still true
draggedIdSV.value still holds the previous item's DraxView ID
SortableItem.useAnimatedStyle evaluates isDragged = hoverReadySV.value && draggedIdSV.value === viewIdSV.value as true for the previously-dragged item
- That item gets
opacity: 0 — it vanishes
The codebase already acknowledges this issue for draggedItem (onMonitorDragStart comment: "Reanimated 4 doesn't reliably sync SharedValue writes from runOnUI worklets") but the same race applies to hoverReadySV and draggedIdSV.
Race 2: stale measurement recorded due to concurrent shiftsRef read/write
On web, onMeasure fires on every layout change caused by Reanimated transforms. The sequence:
- Drag ends →
runOnUI dispatched to set shiftsRef.value = finalShifts
- RAF →
onReorder → consumer calls setState → React re-renders → DOM transforms change → onLayout → onMeasure
onMeasure reads shiftsRef.value[itemKey] (line 238) to subtract the shift from the raw measurement
- Race: if the
runOnUI worklet hasn't completed, shiftsRef.value may be the intermediate/wrong value
- Measurement stored with incorrect baseline
- Next drag's
computeShiftsForOrder uses wrong measurement → computes wrong target offset → item positioned off-screen
Suggested fixes
For Race 1: Reset hoverReadySV and draggedIdSV on the JS thread (direct assignment) before dispatching the runOnUI block, rather than relying on the worklet to clear them. Or reset them in onMonitorDragStart alongside the existing draggedItem handling.
For Race 2: Use a plain JS ref mirror for shifts (safe to read from onMeasure without Reanimated warnings or race conditions). See also issue #249.
Related
Environment
Symptom
When dragging list items rapidly (releasing one drag and starting another quickly), an item disappears from the list. The visual glitch is permanent until the next data refresh.
Root cause — two related races
Race 1: stale hoverReadySV / draggedIdSV between rapid drags
In
finalizeDrag(SortableContainer.tsx), the drag state is cleared via arunOnUIworklet:The
runOnUIworklet is dispatched before the RAF but executes asynchronously on the UI thread. If the user starts a second drag before the worklet completes:hoverReadySV.valueis stilltruedraggedIdSV.valuestill holds the previous item's DraxView IDSortableItem.useAnimatedStyleevaluatesisDragged = hoverReadySV.value && draggedIdSV.value === viewIdSV.valueastruefor the previously-dragged itemopacity: 0— it vanishesThe codebase already acknowledges this issue for
draggedItem(onMonitorDragStartcomment: "Reanimated 4 doesn't reliably sync SharedValue writes from runOnUI worklets") but the same race applies tohoverReadySVanddraggedIdSV.Race 2: stale measurement recorded due to concurrent shiftsRef read/write
On web,
onMeasurefires on every layout change caused by Reanimated transforms. The sequence:runOnUIdispatched to setshiftsRef.value = finalShiftsonReorder→ consumer callssetState→ React re-renders → DOM transforms change →onLayout→onMeasureonMeasurereadsshiftsRef.value[itemKey](line 238) to subtract the shift from the raw measurementrunOnUIworklet hasn't completed,shiftsRef.valuemay be the intermediate/wrong valuecomputeShiftsForOrderuses wrong measurement → computes wrong target offset → item positioned off-screenSuggested fixes
For Race 1: Reset
hoverReadySVanddraggedIdSVon the JS thread (direct assignment) before dispatching therunOnUIblock, rather than relying on the worklet to clear them. Or reset them inonMonitorDragStartalongside the existingdraggedItemhandling.For Race 2: Use a plain JS ref mirror for shifts (safe to read from
onMeasurewithout Reanimated warnings or race conditions). See also issue #249.Related
shiftsRef.valueread inonMeasure