React Native Reanimated DnDReact Native Reanimated DnD
API ReferenceComponents

Sortable Component

A high-level component for creating sortable lists with smooth reordering animations and auto-scrolling support.

Overview

The Sortable component provides a complete solution for sortable lists, handling all the complex state management, gesture handling, and animations internally. It renders a scrollable list where items can be dragged to reorder them with smooth animations and auto-scrolling support.

Import

import { Sortable } from "react-native-reanimated-dnd";

Props

Core Props

data

  • Type: TData[] (where TData extends { id: string })
  • Required: Yes
  • Description: Array of data items to render in the sortable list. Each item must have an id property for tracking.
const tasks = [
  { id: "1", title: "Learn React Native", completed: false },
  { id: "2", title: "Build an app", completed: false },
  { id: "3", title: "Deploy to store", completed: false },
];

<Sortable data={tasks} renderItem={renderTask} itemHeight={60} />;

renderItem

  • Type: (props: SortableRenderItemProps<TData>) => React.ReactElement
  • Required: Yes
  • Description: Function that renders each item in the list. Receives item data and sortable props.
const renderTask = ({ item, id, positions, ...props }) => (
  <SortableItem key={id} id={id} positions={positions} {...props}>
    <View style={styles.taskItem}>
      <Text>{item.title}</Text>
      <Text>{item.completed ? "✓" : "○"}</Text>
    </View>
  </SortableItem>
);

itemHeight

  • Type: number
  • Required: Yes
  • Description: Height of each item in pixels. Used for position calculations and auto-scrolling.
<Sortable
  data={data}
  renderItem={renderItem}
  itemHeight={80} // Each item is 80px tall
/>

Optional Props

style

  • Type: StyleProp<ViewStyle>
  • Default: undefined
  • Description: Style applied to the outer container of the sortable list.
<Sortable
  data={data}
  renderItem={renderItem}
  itemHeight={60}
  style={{
    flex: 1,
    backgroundColor: "#f5f5f5",
    paddingHorizontal: 16,
  }}
/>

contentContainerStyle

  • Type: StyleProp<ViewStyle>
  • Default: undefined
  • Description: Style applied to the scroll view's content container.
<Sortable
  data={data}
  renderItem={renderItem}
  itemHeight={60}
  contentContainerStyle={{
    paddingVertical: 20,
    paddingBottom: 100, // Extra space at bottom
  }}
/>

itemKeyExtractor

  • Type: (item: TData) => string
  • Default: (item) => item.id
  • Description: Function to extract unique keys from items. Useful when your data doesn't use id as the key field.
interface CustomItem {
  uuid: string;
  name: string;
}

<Sortable
  data={customItems}
  renderItem={renderItem}
  itemHeight={60}
  itemKeyExtractor={(item) => item.uuid} // Use uuid instead of id
/>;

SortableRenderItemProps

The render function receives these props:

interface SortableRenderItemProps<TData> {
  item: TData; // The data item
  id: string; // Unique identifier
  positions: SharedValue<{ [id: string]: number }>; // Position mapping
  scrollY: SharedValue<number>; // Scroll position
  scrollViewHeight: number; // Container height
  itemHeight: number; // Height of each item
  itemsCount: number; // Total number of items
}

Usage Examples

Basic Sortable List

import { Sortable, SortableItem } from "react-native-reanimated-dnd";

interface Task {
  id: string;
  title: string;
  completed: boolean;
}

function TaskList() {
  const [tasks, setTasks] = useState<Task[]>([
    { id: "1", title: "Learn React Native", completed: false },
    { id: "2", title: "Build an app", completed: false },
    { id: "3", title: "Deploy to store", completed: false },
  ]);

  const renderTask = ({ item, id, positions, ...props }) => (
    <SortableItem key={id} id={id} positions={positions} {...props}>
      <View style={styles.taskItem}>
        <Text style={styles.taskTitle}>{item.title}</Text>
        <Text style={styles.taskStatus}>
          {item.completed ? "✓ Completed" : "○ Pending"}
        </Text>
      </View>
    </SortableItem>
  );

  return (
    <View style={styles.container}>
      <Text style={styles.header}>My Tasks</Text>
      <Sortable
        data={tasks}
        renderItem={renderTask}
        itemHeight={60}
        style={styles.list}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
  },
  header: {
    fontSize: 24,
    fontWeight: "bold",
    padding: 16,
  },
  list: {
    flex: 1,
    paddingHorizontal: 16,
  },
  taskItem: {
    backgroundColor: "#f8f9fa",
    padding: 16,
    marginVertical: 4,
    borderRadius: 8,
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
  },
  taskTitle: {
    fontSize: 16,
    fontWeight: "500",
  },
  taskStatus: {
    fontSize: 14,
    color: "#666",
  },
});

Sortable List with Callbacks

function AdvancedTaskList() {
  const [tasks, setTasks] = useState(initialTasks);
  const [draggingTask, setDraggingTask] = useState<string | null>(null);

  const renderTask = ({ item, id, positions, ...props }) => (
    <SortableItem
      key={id}
      id={id}
      positions={positions}
      {...props}
      onMove={(itemId, fromIndex, toIndex) => {
        // Update data when items are reordered
        const newTasks = [...tasks];
        const [movedTask] = newTasks.splice(fromIndex, 1);
        newTasks.splice(toIndex, 0, movedTask);
        setTasks(newTasks);

        // Analytics tracking
        analytics.track("task_reordered", {
          taskId: itemId,
          from: fromIndex,
          to: toIndex,
          totalTasks: tasks.length,
        });
      }}
      onDragStart={(itemId) => {
        // Haptic feedback
        if (Platform.OS === "ios") {
          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
        }
        setDraggingTask(itemId);
      }}
      onDrop={(itemId) => {
        setDraggingTask(null);
        // Save to backend
        saveTasks(tasks);
      }}
    >
      <Animated.View
        style={[
          styles.taskItem,
          item.priority === "high" && styles.highPriorityTask,
          draggingTask === id && styles.draggingTask,
        ]}
      >
        <View style={styles.taskContent}>
          <Text style={styles.taskTitle}>{item.title}</Text>
          <Text style={styles.taskDue}>Due: {item.dueDate}</Text>
          <View style={styles.taskMeta}>
            <Text style={styles.taskPriority}>{item.priority}</Text>
            <Text style={styles.taskCategory}>{item.category}</Text>
          </View>
        </View>

        <View style={styles.taskActions}>
          <TouchableOpacity onPress={() => toggleComplete(item.id)}>
            <Icon
              name={item.completed ? "check-circle" : "circle"}
              size={24}
              color={item.completed ? "#4CAF50" : "#ccc"}
            />
          </TouchableOpacity>
        </View>
      </Animated.View>
    </SortableItem>
  );

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>My Tasks ({tasks.length})</Text>
        <TouchableOpacity onPress={addNewTask}>
          <Icon name="plus" size={24} color="#007AFF" />
        </TouchableOpacity>
      </View>

      <Sortable
        data={tasks}
        renderItem={renderTask}
        itemHeight={100}
        style={styles.sortableList}
        contentContainerStyle={styles.listContent}
      />
    </View>
  );
}

Sortable List with Drag Handles

function SortableWithHandles() {
  const [items, setItems] = useState(data);

  const renderItem = ({ item, id, positions, ...props }) => (
    <SortableItem key={id} id={id} positions={positions} {...props}>
      <View style={styles.itemContainer}>
        <View style={styles.itemContent}>
          <Text style={styles.itemTitle}>{item.title}</Text>
          <Text style={styles.itemSubtitle}>{item.subtitle}</Text>
        </View>

        {/* Only this handle area can initiate dragging */}
        <SortableItem.Handle style={styles.dragHandle}>
          <View style={styles.handleIcon}>
            <View style={styles.handleDot} />
            <View style={styles.handleDot} />
            <View style={styles.handleDot} />
            <View style={styles.handleDot} />
            <View style={styles.handleDot} />
            <View style={styles.handleDot} />
          </View>
        </SortableItem.Handle>
      </View>
    </SortableItem>
  );

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Drag by Handle Only</Text>
      <Sortable
        data={items}
        renderItem={renderItem}
        itemHeight={70}
        style={styles.list}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  itemContainer: {
    flexDirection: "row",
    alignItems: "center",
    backgroundColor: "#fff",
    padding: 16,
    marginVertical: 2,
    borderRadius: 8,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 2,
  },
  itemContent: {
    flex: 1,
  },
  itemTitle: {
    fontSize: 16,
    fontWeight: "600",
    marginBottom: 4,
  },
  itemSubtitle: {
    fontSize: 14,
    color: "#666",
  },
  dragHandle: {
    padding: 8,
    marginLeft: 12,
  },
  handleIcon: {
    flexDirection: "row",
    flexWrap: "wrap",
    width: 12,
    height: 12,
  },
  handleDot: {
    width: 3,
    height: 3,
    backgroundColor: "#999",
    borderRadius: 1.5,
    margin: 1,
  },
});

Custom Key Extractor

interface CustomItem {
  uuid: string;
  name: string;
  order: number;
  category: string;
}

function CustomSortableList() {
  const [items, setItems] = useState<CustomItem[]>(data);

  const renderItem = ({ item, id, positions, ...props }) => (
    <SortableItem key={id} id={id} positions={positions} {...props}>
      <View style={styles.customItem}>
        <View style={styles.itemInfo}>
          <Text style={styles.itemName}>{item.name}</Text>
          <Text style={styles.itemCategory}>{item.category}</Text>
        </View>
        <Text style={styles.itemOrder}>#{item.order}</Text>
      </View>
    </SortableItem>
  );

  return (
    <Sortable
      data={items}
      renderItem={renderItem}
      itemHeight={60}
      itemKeyExtractor={(item) => item.uuid} // Use uuid instead of id
      style={styles.list}
    />
  );
}
interface Photo {
  id: string;
  uri: string;
  title: string;
  size: number;
}

function PhotoGallery() {
  const [photos, setPhotos] = useState<Photo[]>(photoData);

  const renderPhoto = ({ item, id, positions, ...props }) => (
    <SortableItem key={id} id={id} positions={positions} {...props}>
      <View style={styles.photoItem}>
        <Image source={{ uri: item.uri }} style={styles.photoImage} />
        <View style={styles.photoInfo}>
          <Text style={styles.photoTitle}>{item.title}</Text>
          <Text style={styles.photoSize}>{formatFileSize(item.size)}</Text>
        </View>
        <View style={styles.photoActions}>
          <TouchableOpacity onPress={() => editPhoto(item.id)}>
            <Icon name="edit" size={20} color="#007AFF" />
          </TouchableOpacity>
          <TouchableOpacity onPress={() => deletePhoto(item.id)}>
            <Icon name="trash" size={20} color="#FF3B30" />
          </TouchableOpacity>
        </View>
      </View>
    </SortableItem>
  );

  return (
    <View style={styles.container}>
      <Text style={styles.galleryTitle}>Photo Gallery ({photos.length})</Text>
      <Sortable
        data={photos}
        renderItem={renderPhoto}
        itemHeight={80}
        style={styles.gallery}
        contentContainerStyle={styles.galleryContent}
      />
    </View>
  );
}

Performance Optimized List

function LargeSortableList() {
  const [items, setItems] = useState(generateLargeDataset(1000));

  // Memoize render function for performance
  const renderItem = useCallback(
    ({ item, id, positions, ...props }) => (
      <SortableItem key={id} id={id} positions={positions} {...props}>
        <MemoizedListItem item={item} />
      </SortableItem>
    ),
    []
  );

  return (
    <Sortable
      data={items}
      renderItem={renderItem}
      itemHeight={50}
      style={styles.performanceList}
    />
  );
}

// Memoized item component for better performance
const MemoizedListItem = React.memo(({ item }) => (
  <View style={styles.performanceItem}>
    <Text style={styles.itemText}>{item.title}</Text>
    <Text style={styles.itemIndex}>#{item.index}</Text>
  </View>
));

Auto-Scrolling

The Sortable component automatically handles scrolling when dragging items near the edges:

  • Scroll Threshold: 50px from top/bottom edges
  • Scroll Speed: Adaptive based on proximity to edge
  • Smooth Scrolling: Uses native scroll animations
// Auto-scrolling is enabled by default
<Sortable
  data={data}
  renderItem={renderItem}
  itemHeight={60}
  // Auto-scrolling works automatically when dragging near edges
/>

Performance Considerations

Large Lists

For lists with many items (>100), consider:

  1. Memoization: Use React.memo for item components
  2. Key Extraction: Ensure stable, unique keys
  3. Minimal Re-renders: Avoid inline functions in render
// Good: Memoized component
const MemoizedItem = React.memo(({ item }) => (
  <View style={styles.item}>
    <Text>{item.title}</Text>
  </View>
));

// Good: Stable render function
const renderItem = useCallback(
  ({ item, id, positions, ...props }) => (
    <SortableItem key={id} id={id} positions={positions} {...props}>
      <MemoizedItem item={item} />
    </SortableItem>
  ),
  []
);

Memory Management

// Clean up resources when component unmounts
useEffect(() => {
  return () => {
    // Cleanup any subscriptions or timers
    clearTimeout(saveTimeout);
  };
}, []);

TypeScript Support

The Sortable component is fully typed with generics:

interface Task {
  id: string;
  title: string;
  completed: boolean;
  priority: "low" | "medium" | "high";
}

// TypeScript infers the correct types
<Sortable<Task>
  data={tasks}
  renderItem={({ item, id, positions, ...props }) => {
    // item is correctly typed as Task
    // id is string
    // positions is SharedValue<{ [id: string]: number }>
    return (
      <SortableItem key={id} id={id} positions={positions} {...props}>
        <TaskComponent task={item} />
      </SortableItem>
    );
  }}
  itemHeight={60}
/>;

Accessibility

The Sortable component includes accessibility features:

<Sortable
  data={data}
  renderItem={({ item, id, positions, ...props }) => (
    <SortableItem
      key={id}
      id={id}
      positions={positions}
      {...props}
      accessible={true}
      accessibilityRole="button"
      accessibilityLabel={`Reorder ${item.title}`}
      accessibilityHint="Double tap and hold to drag"
    >
      <Text>{item.title}</Text>
    </SortableItem>
  )}
  itemHeight={60}
/>

See Also