SortableItem
The SortableItem
component represents individual items within a sortable list, providing drag-and-drop functionality, gesture handling, and smooth reordering animations.
Overview
SortableItem provides the drag-and-drop functionality for individual list items within a Sortable component. It handles gesture recognition, position animations, reordering logic, and can be used with or without drag handles for different interaction patterns.
Basic Usage
import { SortableItem } from 'react-native-reanimated-dnd';
function TaskItem({ task, positions, ...sortableProps }) {
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
>
<View style={styles.taskContainer}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskStatus}>
{task.completed ? 'Done' : 'Pending'}
</Text>
</View>
</SortableItem>
);
}
Props Reference
Core Props
Prop | Type | Default | Description |
---|---|---|---|
id | string | Required | Unique identifier for the item |
positions | SharedValue<number[]> | Required | Shared value for item positions |
children | ReactNode | Required | Content to render inside the item |
data | T | - | Data associated with this item |
Layout Props
Prop | Type | Default | Description |
---|---|---|---|
itemHeight | number | Required | Height of the item in pixels |
itemsCount | number | Required | Total number of items in the list |
containerHeight | SharedValue<number> | Required | Height of the container |
lowerBound | SharedValue<number> | Required | Lower scroll boundary |
autoScrollDirection | SharedValue<ScrollDirection> | Required | Auto-scroll direction state |
Styling Props
Prop | Type | Default | Description |
---|---|---|---|
style | StyleProp<ViewStyle> | - | Style for the item container |
animatedStyle | AnimatedStyleProp<ViewStyle> | - | Animated styles for the item |
Callback Props
Prop | Type | Default | Description |
---|---|---|---|
onMove | (id: string, from: number, to: number) => void | - | Called when item is moved |
onDragStart | (id: string, position: number) => void | - | Called when dragging starts |
onDrop | (id: string, position: number) => void | - | Called when dragging ends |
onDragging | (id: string, overItemId?: string, yPosition?: number) => void | - | Called during dragging |
Examples
Basic Sortable Item
function TaskItem({ task, positions, ...sortableProps }) {
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
onMove={(id, from, to) => {
console.log(`Task ${id} moved from ${from} to ${to}`);
reorderTasks(id, from, to);
}}
>
<View style={styles.taskContainer}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskStatus}>
{task.completed ? 'Done' : 'Pending'}
</Text>
</View>
</SortableItem>
);
}
const styles = StyleSheet.create({
taskContainer: {
backgroundColor: 'white',
padding: 16,
marginHorizontal: 16,
marginVertical: 4,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
taskTitle: {
fontSize: 16,
fontWeight: '500',
flex: 1,
},
taskStatus: {
fontSize: 14,
color: '#666',
},
});
Sortable Item with Drag Handle
function TaskItemWithHandle({ task, positions, ...sortableProps }) {
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
>
<View style={styles.taskContainer}>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskDescription}>{task.description}</Text>
</View>
{/* Only this handle can initiate dragging */}
<SortableItem.Handle style={styles.dragHandle}>
<View style={styles.handleIcon}>
<View style={styles.handleLine} />
<View style={styles.handleLine} />
<View style={styles.handleLine} />
</View>
</SortableItem.Handle>
</View>
</SortableItem>
);
}
const styles = StyleSheet.create({
taskContainer: {
backgroundColor: 'white',
padding: 16,
marginHorizontal: 16,
marginVertical: 4,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
taskDescription: {
fontSize: 14,
color: '#666',
},
dragHandle: {
padding: 8,
marginLeft: 12,
},
handleIcon: {
width: 20,
height: 20,
justifyContent: 'space-between',
alignItems: 'center',
},
handleLine: {
width: 16,
height: 2,
backgroundColor: '#999',
borderRadius: 1,
},
});
Advanced Item with State Tracking
function AdvancedTaskItem({ task, positions, ...sortableProps }) {
const [isDragging, setIsDragging] = useState(false);
const [isHovered, setIsHovered] = useState(false);
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
onDragStart={(id, position) => {
setIsDragging(true);
hapticFeedback();
analytics.track('drag_start', { taskId: id, position });
}}
onDrop={(id, position) => {
setIsDragging(false);
analytics.track('drag_end', { taskId: id, position });
}}
onDragging={(id, overItemId, yPosition) => {
if (overItemId) {
// Show visual feedback for item being hovered over
setIsHovered(overItemId === task.id);
}
}}
onMove={(id, from, to) => {
// Update data and sync to backend
reorderTasks(id, from, to);
syncToBackend();
}}
style={[
styles.taskItem,
isDragging && styles.draggingItem,
isHovered && styles.hoveredItem
]}
>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskPriority}>Priority: {task.priority}</Text>
<Text style={styles.taskDue}>Due: {task.dueDate}</Text>
{isDragging && (
<Text style={styles.dragIndicator}>Dragging...</Text>
)}
</View>
<View style={styles.taskMeta}>
<Text style={styles.taskAssignee}>{task.assignee}</Text>
<View style={[styles.priorityDot, getPriorityColor(task.priority)]} />
</View>
</SortableItem>
);
}
const styles = StyleSheet.create({
taskItem: {
backgroundColor: 'white',
borderRadius: 8,
padding: 16,
marginHorizontal: 16,
marginVertical: 4,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
draggingItem: {
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
transform: [{ scale: 1.02 }],
backgroundColor: '#f8f9fa',
},
hoveredItem: {
backgroundColor: 'rgba(59, 130, 246, 0.05)',
borderColor: '#3b82f6',
borderWidth: 1,
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
taskPriority: {
fontSize: 12,
color: '#666',
marginBottom: 2,
},
taskDue: {
fontSize: 12,
color: '#999',
},
dragIndicator: {
fontSize: 12,
color: '#3b82f6',
fontWeight: 'bold',
marginTop: 4,
},
taskMeta: {
alignItems: 'flex-end',
},
taskAssignee: {
fontSize: 12,
color: '#666',
marginBottom: 4,
},
priorityDot: {
width: 8,
height: 8,
borderRadius: 4,
},
});
function getPriorityColor(priority) {
switch (priority) {
case 'high':
return { backgroundColor: '#ef4444' };
case 'medium':
return { backgroundColor: '#f59e0b' };
case 'low':
return { backgroundColor: '#10b981' };
default:
return { backgroundColor: '#6b7280' };
}
}
Custom Animated Styles
function AnimatedTaskItem({ task, positions, ...sortableProps }) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
animatedStyle={animatedStyle}
onDragStart={() => {
scale.value = withSpring(1.05);
opacity.value = withTiming(0.9);
}}
onDrop={() => {
scale.value = withSpring(1);
opacity.value = withTiming(1);
}}
>
<View style={styles.taskItem}>
<Text style={styles.taskTitle}>{task.title}</Text>
</View>
</SortableItem>
);
}
File List Item
function FileListItem({ file, positions, ...sortableProps }) {
const [isSelected, setIsSelected] = useState(false);
return (
<SortableItem
id={file.id}
data={file}
positions={positions}
{...sortableProps}
onMove={(id, from, to) => {
reorderFiles(id, from, to);
showToast('File reordered');
}}
>
<Pressable
style={[styles.fileItem, isSelected && styles.selectedFile]}
onPress={() => setIsSelected(!isSelected)}
>
<View style={styles.fileIcon}>
<Icon name={getFileIcon(file.type)} size={24} color={getFileColor(file.type)} />
</View>
<View style={styles.fileInfo}>
<Text style={styles.fileName}>{file.name}</Text>
<Text style={styles.fileSize}>{formatFileSize(file.size)}</Text>
<Text style={styles.fileDate}>{formatDate(file.modifiedDate)}</Text>
</View>
<SortableItem.Handle style={styles.dragHandle}>
<Icon name="drag-handle" size={20} color="#999" />
</SortableItem.Handle>
</Pressable>
</SortableItem>
);
}
const styles = StyleSheet.create({
fileItem: {
backgroundColor: 'white',
padding: 12,
marginHorizontal: 16,
marginVertical: 2,
borderRadius: 6,
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#e5e7eb',
},
selectedFile: {
backgroundColor: '#eff6ff',
borderColor: '#3b82f6',
},
fileIcon: {
marginRight: 12,
},
fileInfo: {
flex: 1,
},
fileName: {
fontSize: 14,
fontWeight: '500',
marginBottom: 2,
},
fileSize: {
fontSize: 12,
color: '#666',
},
fileDate: {
fontSize: 11,
color: '#999',
},
dragHandle: {
padding: 8,
marginLeft: 8,
},
});
Handle Component
The SortableItem.Handle
component creates specific draggable areas within the item:
Handle Props
Prop | Type | Description |
---|---|---|
children | ReactNode | Content to render inside the handle |
style | StyleProp<ViewStyle> | Style for the handle container |
Handle Examples
// Icon handle
<SortableItem.Handle style={styles.iconHandle}>
<Icon name="drag-handle" size={20} color="#666" />
</SortableItem.Handle>
// Custom dots handle
<SortableItem.Handle style={styles.dotsHandle}>
<View style={styles.dotsGrid}>
{Array.from({ length: 6 }).map((_, i) => (
<View key={i} style={styles.dot} />
))}
</View>
</SortableItem.Handle>
// Lines handle
<SortableItem.Handle style={styles.linesHandle}>
<View style={styles.linesContainer}>
<View style={styles.line} />
<View style={styles.line} />
<View style={styles.line} />
</View>
</SortableItem.Handle>
// Text handle
<SortableItem.Handle style={styles.textHandle}>
<Text style={styles.handleText}>≡</Text>
</SortableItem.Handle>
Handle Styling
const handleStyles = StyleSheet.create({
iconHandle: {
padding: 8,
borderRadius: 4,
backgroundColor: '#f3f4f6',
},
dotsHandle: {
padding: 8,
},
dotsGrid: {
width: 16,
height: 16,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignContent: 'space-between',
},
dot: {
width: 2,
height: 2,
backgroundColor: '#9ca3af',
borderRadius: 1,
},
linesHandle: {
padding: 8,
},
linesContainer: {
width: 16,
height: 12,
justifyContent: 'space-between',
},
line: {
height: 2,
backgroundColor: '#9ca3af',
borderRadius: 1,
},
textHandle: {
padding: 8,
},
handleText: {
fontSize: 16,
color: '#9ca3af',
fontWeight: 'bold',
},
});
Callback Details
onMove Callback
Called when the item is successfully moved to a new position:
const handleMove = (itemId: string, fromIndex: number, toIndex: number) => {
console.log(`Item ${itemId} moved from position ${fromIndex} to ${toIndex}`);
// Update your data array
const newData = [...data];
const [movedItem] = newData.splice(fromIndex, 1);
newData.splice(toIndex, 0, movedItem);
setData(newData);
// Sync to backend
updateItemOrder(itemId, toIndex);
};
onDragStart Callback
Called when dragging begins:
const handleDragStart = (itemId: string, position: number) => {
console.log(`Started dragging item ${itemId} at position ${position}`);
// Haptic feedback
hapticFeedback();
// Update UI state
setDraggedItemId(itemId);
// Analytics
analytics.track('sortable_drag_start', { itemId, position });
};
onDrop Callback
Called when dragging ends:
const handleDrop = (itemId: string, position: number) => {
console.log(`Dropped item ${itemId} at position ${position}`);
// Clear UI state
setDraggedItemId(null);
// Success feedback
showToast('Item reordered successfully');
// Analytics
analytics.track('sortable_drag_end', { itemId, position });
};
onDragging Callback
Called continuously during dragging:
const handleDragging = (
itemId: string,
overItemId?: string,
yPosition?: number
) => {
console.log(`Dragging ${itemId}, over ${overItemId}, at y: ${yPosition}`);
// Update hover states
if (overItemId) {
setHoveredItemId(overItemId);
}
// Update drag position for overlays
if (yPosition !== undefined) {
updateDragOverlayPosition(yPosition);
}
};
TypeScript Support
The SortableItem component is fully typed with generic support:
interface TaskData {
id: string;
title: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
assignee?: string;
}
// Fully typed sortable item
<SortableItem<TaskData>
id={task.id}
data={task}
positions={positions}
{...props}
onMove={(id: string, from: number, to: number) => {
// Fully typed callback
reorderTasks(id, from, to);
}}
>
<TaskComponent task={task} />
</SortableItem>
Performance Tips
- Use
React.memo
for item content that doesn't change frequently - Avoid heavy computations in drag callbacks
- Use
useCallback
for stable callback references - Keep item heights consistent for better performance
- Minimize the number of animated styles
Accessibility
The SortableItem component supports accessibility features:
<SortableItem
id={item.id}
data={item}
positions={positions}
{...props}
>
<View
accessible={true}
accessibilityRole="button"
accessibilityLabel={`Sortable item: ${item.title}`}
accessibilityHint="Double tap and hold to reorder this item"
accessibilityActions={[
{ name: 'move-up', label: 'Move up' },
{ name: 'move-down', label: 'Move down' },
]}
onAccessibilityAction={(event) => {
if (event.nativeEvent.actionName === 'move-up') {
moveItemUp(item.id);
} else if (event.nativeEvent.actionName === 'move-down') {
moveItemDown(item.id);
}
}}
>
<Text>{item.title}</Text>
</View>
</SortableItem>
Common Patterns
Conditional Handles
function ConditionalHandleItem({ item, canReorder, ...props }) {
return (
<SortableItem {...props}>
<View style={styles.item}>
<Text>{item.title}</Text>
{canReorder && (
<SortableItem.Handle style={styles.handle}>
<Icon name="drag-handle" />
</SortableItem.Handle>
)}
</View>
</SortableItem>
);
}
Multi-select Support
function MultiSelectItem({ item, isSelected, onSelect, ...props }) {
return (
<SortableItem {...props}>
<Pressable
style={[styles.item, isSelected && styles.selected]}
onPress={() => onSelect(item.id)}
>
<Checkbox value={isSelected} />
<Text>{item.title}</Text>
<SortableItem.Handle style={styles.handle}>
<Icon name="drag-handle" />
</SortableItem.Handle>
</Pressable>
</SortableItem>
);
}
See Also
- Sortable - Parent sortable list component
- useSortable Hook - Underlying hook for custom implementations
- Basic Concepts - Understanding sortable lists
- Examples - More comprehensive examples