Skip to content

Drag positions are inaccurate after adding items to middle of list #66

@brimby

Description

@brimby

Description

If I add items to the middle of my list, afterward any item below/after the added items that I try to drag UP ABOVE the new item(s) has misaligned drag breaks between items. Currently my fix is to give a key to the list that is based on the data.length, but it looks kind of bad when the list remounts because of the key change so it would be nice to have this fixed in the library if possible.

Steps to reproduce

  1. Use reproducible example provided
  2. Hit "Add to middle" button (tip: hitting it 3 or so times to add several to the middle actually helps make the problem clearer)
  3. Drag an item that's UNDER the new items UPWARD. Look for the "New" tag on the items to know which ones you should be looking underneath for drag candidates.
  4. The drag positions that cause item shifts should be wrong. It usually gets more noticeable the more new items are added and the higher you try to drag (usually dragging it into the top position becomes impossible).
  5. You might have to try a couple times with different numbers of items, because sometimes the wrongness can be slight and/or masked depending on how the exact heights line up. So if you don't see anything, hit the reset button and try again with a different number of new items perhaps. Note that after dragEnded the list won't have problems anymore until you add more new items again.

Minimal reproducible example

import React from 'react'
import { View, StyleSheet, Pressable, Text } from 'react-native'
import { ScrollViewContainer, NestedReorderableList, useReorderableDrag, reorderItems } from 'react-native-reorderable-list'

// Default items with varying heights (in px)
const INITIAL_ITEMS = [
  { key: 'a', label: 'Short', height: 30 },
  { key: 'b', label: 'Medium', height: 50 },
  { key: 'c', label: 'Tall', height: 100 },
  { key: 'd', label: 'Short', height: 30 },
  { key: 'e', label: 'Medium-tall', height: 70 },
]

const TestItem = React.memo(({ item }) => {
  const dragHandler = useReorderableDrag()
  return (
    <View
      style={[styles.item, { height: item.height }]}
    >
      <Text>{item.label} ({item.height}px)</Text>
      <Pressable onPressIn={dragHandler} delayLongPress={0} style={styles.dragHandle}>
        <Text style={styles.dragHandleText}>⋮⋮</Text>
      </Pressable>
    </View>
  )
})

let idCounter = 0
function nextKey() {
  return 'item-' + ++idCounter + '-' + Date.now()
}

export default function ReorderableListTestPage() {
  const [items, setItems] = React.useState(INITIAL_ITEMS)

  const addToMiddle = () => {
    const mid = Math.floor(items.length / 2)
    const newItem = {
      key: nextKey(),
      label: `New #${items.length + 1}`,
      height: 50 + (items.length % 3) * 15, // 50, 65, or 80
    }
    setItems((prev) => {
      const next = [...prev]
      next.splice(mid, 0, newItem)
      return next
    })
  }

  const handleReorder = ({ from, to }) => {
    setItems((prev) => reorderItems(prev, from, to))
  }

  const renderItem = ({ item }) => <TestItem item={item} />

  return (

      <View style={{ flex: 1 }}>
        <View style={styles.header}>
          <Text style={styles.title}>Reorderable List Test</Text>
          <View style={styles.buttonContainer}>
            <Pressable onPress={addToMiddle} style={styles.button}><Text style={styles.buttonText}>Add to middle</Text></Pressable>
            <Pressable onPress={() => setItems(INITIAL_ITEMS)} style={styles.button}><Text style={styles.buttonText}>Reset list</Text></Pressable>
          </View>
        </View>
        <Text style={styles.hint}>
          Drag items to reorder. After adding items to the middle, check if drag
          drop targets align correctly.
        </Text>
        <View style={{ flex: 1 }}>
          <ScrollViewContainer style={{ flex: 1 }}>
            <NestedReorderableList
              data={items}
              onReorder={handleReorder}
              keyExtractor={({ key }) => key}
              renderItem={renderItem}
              scrollEnabled={false}
              contentContainerStyle={styles.listContent}
            />
          </ScrollViewContainer>
        </View>
      </View>

  )
}

const styles = StyleSheet.create({
  header: {
    padding: 12,
    gap: 8,
    alignItems: 'center',
    flexShrink: 1
  },
  buttonContainer: {
    flexDirection: 'row',
    gap: 8,
  },
  button: {
    padding: 8,
    backgroundColor: '#cde8ff',
    borderRadius: 4,
    borderWidth: 1,
    borderColor: '#4b4b4b',
  },
  buttonText: {
    fontSize: 14,
    color: '#333',
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
  },
  hint: {
    paddingHorizontal: 12,
    paddingBottom: 8,
    fontSize: 12,
    color: '#666',
  },
  listContent: {
    paddingBottom: 40,
  },
  item: {
    backgroundColor: '#f5f5f5',
    borderBottomWidth: 1,
    borderBottomColor: '#ddd',
    paddingHorizontal: 12,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  dragHandle: {
    padding: 8,
  },
  dragHandleText: {
    fontSize: 15,
    color: '#666',
    letterSpacing: -2,
  },
})

Relevant logs

No response

Reorderable List version

16.0

React Native version

81.5

Project type

Expo

Architecture

Fabric (New Architecture)

Reanimated version

4.1.1

Gesture Handler version

2.28.0

Affected platforms

Android

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions