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[]
(whereTData 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}
/>
);
}
Photo Gallery Sortable
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:
- Memoization: Use
React.memo
for item components - Key Extraction: Ensure stable, unique keys
- 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
- SortableItem Component - Individual sortable items
- useSortableList Hook - Underlying hook
- SortableRenderItemProps - Render function props
- Performance Guide - Optimization tips