Skip to main content

SortableItem Component

A component for individual items within a sortable list, providing drag-and-drop functionality with gesture recognition, position animations, and reordering logic.

Overview​

The SortableItem component handles the drag-and-drop functionality for individual list items. It can be used with or without drag handles for different interaction patterns and integrates seamlessly with the Sortable component or useSortableList hook.

Import​

import { SortableItem } from 'react-native-reanimated-dnd';

Props​

Core Props​

id​

  • Type: string
  • Required: Yes
  • Description: Unique identifier for this sortable item. Used for tracking position and reordering.
<SortableItem id="task-1" positions={positions} {...props}>
<Text>Task content</Text>
</SortableItem>

positions​

  • Type: SharedValue<{ [id: string]: number }>
  • Required: Yes
  • Description: Shared value containing the position mapping for all items in the sortable list.

children​

  • Type: React.ReactNode
  • Required: Yes
  • Description: The content to render inside the sortable item.

Layout Props​

itemHeight​

  • Type: number
  • Required: Yes
  • Description: Height of this item in pixels. Used for position calculations and animations.

itemsCount​

  • Type: number
  • Required: Yes
  • Description: Total number of items in the sortable list.

containerHeight​

  • Type: number
  • Required: Yes
  • Description: Height of the container holding the sortable list.

Scroll Props​

lowerBound​

  • Type: SharedValue<number>
  • Required: Yes
  • Description: Lower boundary for auto-scrolling calculations.

autoScrollDirection​

  • Type: SharedValue<ScrollDirection>
  • Required: Yes
  • Description: Current auto-scroll direction state.

Optional Props​

data​

  • Type: T
  • Required: No
  • Description: Data payload associated with this item. Passed to callback functions.

style​

  • Type: StyleProp<ViewStyle>
  • Required: No
  • Description: Style applied to the item container.

animatedStyle​

  • Type: AnimatedStyle<ViewStyle>
  • Required: No
  • Description: Custom animated styles applied to the item.

Callback Props​

onMove​

  • Type: (id: string, from: number, to: number) => void
  • Required: No
  • Description: Callback fired when the item is moved to a new position.
<SortableItem
id="task-1"
positions={positions}
onMove={(id, from, to) => {
console.log(`Item ${id} moved from position ${from} to ${to}`);
reorderItems(id, from, to);
}}
{...props}
>
<Text>Moveable item</Text>
</SortableItem>

onDragStart​

  • Type: (id: string, position: number) => void
  • Required: No
  • Description: Callback fired when dragging starts.
<SortableItem
id="task-1"
positions={positions}
onDragStart={(id, position) => {
console.log(`Started dragging item ${id} at position ${position}`);
hapticFeedback();
}}
{...props}
>
<Text>Draggable item</Text>
</SortableItem>

onDrop​

  • Type: (id: string, position: number) => void
  • Required: No
  • Description: Callback fired when dragging ends.

onDragging​

  • Type: (id: string, overItemId: string | null, yPosition: number) => void
  • Required: No
  • Description: Callback fired continuously while dragging. Provides information about which item is being hovered over.

SortableItem.Handle​

A handle component that creates a specific draggable area within the sortable item. When a handle is present, only the handle area can initiate dragging.

Props​

children​

  • Type: React.ReactNode
  • Required: Yes
  • Description: The content to render inside the handle.

style​

  • Type: StyleProp<ViewStyle>
  • Required: No
  • Description: Style applied to the handle container.

Usage Examples​

Basic Sortable Item​

import { SortableItem } from 'react-native-reanimated-dnd';

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: '#fff',
padding: 16,
marginVertical: 4,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
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: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 16,
marginVertical: 4,
borderRadius: 8,
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
taskDescription: {
fontSize: 14,
color: '#666',
},
dragHandle: {
padding: 8,
marginLeft: 12,
},
handleIcon: {
width: 20,
height: 16,
justifyContent: 'space-between',
},
handleLine: {
height: 2,
backgroundColor: '#999',
borderRadius: 1,
},
});

Advanced Sortable 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) {
setIsHovered(true);
// Show visual feedback for item being hovered over
highlightItem(overItemId);
} else {
setIsHovered(false);
}
}}
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>
{isDragging && (
<Text style={styles.dragIndicator}>Dragging...</Text>
)}
</View>
</SortableItem>
);
}

const styles = StyleSheet.create({
taskItem: {
backgroundColor: '#fff',
padding: 16,
marginVertical: 4,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e0e0e0',
},
draggingItem: {
opacity: 0.8,
transform: [{ scale: 1.05 }],
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 8,
},
hoveredItem: {
borderColor: '#007AFF',
backgroundColor: '#f0f8ff',
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
taskPriority: {
fontSize: 14,
color: '#666',
},
dragIndicator: {
fontSize: 12,
color: '#007AFF',
fontStyle: 'italic',
marginTop: 4,
},
});

File List Sortable Item​

interface FileItem {
id: string;
name: string;
size: number;
type: string;
lastModified: Date;
}

function FileListItem({ file, positions, ...sortableProps }) {
const [isSelected, setIsSelected] = useState(false);

const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};

const getFileIcon = (type: string) => {
switch (type) {
case 'pdf': return '📄';
case 'image': return '🖼️';
case 'video': return '🎥';
case 'audio': return '🎵';
default: return '📁';
}
};

return (
<SortableItem
id={file.id}
data={file}
positions={positions}
{...sortableProps}
onMove={(id, from, to) => {
reorderFiles(id, from, to);
showToast(`${file.name} moved`);
}}
>
<TouchableOpacity
onPress={() => setIsSelected(!isSelected)}
style={[styles.fileItem, isSelected && styles.selectedFile]}
>
<View style={styles.fileIcon}>
<Text style={styles.iconText}>{getFileIcon(file.type)}</Text>
</View>

<View style={styles.fileInfo}>
<Text style={styles.fileName}>{file.name}</Text>
<Text style={styles.fileDetails}>
{formatFileSize(file.size)} • {file.lastModified.toLocaleDateString()}
</Text>
</View>

<SortableItem.Handle style={styles.fileHandle}>
<View style={styles.handleDots}>
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
</View>
</SortableItem.Handle>
</TouchableOpacity>
</SortableItem>
);
}

const styles = StyleSheet.create({
fileItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 12,
marginVertical: 2,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e0e0e0',
},
selectedFile: {
backgroundColor: '#e3f2fd',
borderColor: '#2196f3',
},
fileIcon: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
iconText: {
fontSize: 24,
},
fileInfo: {
flex: 1,
},
fileName: {
fontSize: 16,
fontWeight: '500',
marginBottom: 2,
},
fileDetails: {
fontSize: 12,
color: '#666',
},
fileHandle: {
padding: 8,
},
handleDots: {
flexDirection: 'row',
flexWrap: 'wrap',
width: 12,
height: 12,
},
dot: {
width: 2,
height: 2,
backgroundColor: '#999',
borderRadius: 1,
margin: 1,
},
});

Custom Animated Sortable Item​

function AnimatedTaskItem({ task, positions, ...sortableProps }) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);

const animatedStyle = useAnimatedStyle(() => {
return {
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.animatedTask}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskDescription}>{task.description}</Text>
</View>
</SortableItem>
);
}
function PhotoGalleryItem({ photo, positions, ...sortableProps }) {
const [imageLoaded, setImageLoaded] = useState(false);

return (
<SortableItem
id={photo.id}
data={photo}
positions={positions}
{...sortableProps}
onMove={(id, from, to) => {
reorderPhotos(id, from, to);
}}
>
<View style={styles.photoItem}>
<Image
source={{ uri: photo.uri }}
style={styles.photoImage}
onLoad={() => setImageLoaded(true)}
/>

{!imageLoaded && (
<View style={styles.photoPlaceholder}>
<ActivityIndicator size="small" color="#666" />
</View>
)}

<View style={styles.photoOverlay}>
<Text style={styles.photoTitle}>{photo.title}</Text>

<SortableItem.Handle style={styles.photoHandle}>
<View style={styles.handleIcon}>
<Icon name="drag-handle" size={20} color="#fff" />
</View>
</SortableItem.Handle>
</View>
</View>
</SortableItem>
);
}

const styles = StyleSheet.create({
photoItem: {
position: 'relative',
backgroundColor: '#f0f0f0',
borderRadius: 8,
overflow: 'hidden',
marginVertical: 4,
},
photoImage: {
width: '100%',
height: 120,
resizeMode: 'cover',
},
photoPlaceholder: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f0f0f0',
},
photoOverlay: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
photoTitle: {
color: '#fff',
fontSize: 14,
fontWeight: '500',
flex: 1,
},
photoHandle: {
padding: 4,
},
handleIcon: {
opacity: 0.8,
},
});

TypeScript Support​

The component is fully typed with generic support:

interface TaskData {
id: string;
title: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
}

function TypedSortableItem({ task, positions, ...props }) {
return (
<SortableItem<TaskData>
id={task.id}
data={task}
positions={positions}
{...props}
onMove={(id: string, from: number, to: number) => {
// All parameters are properly typed
console.log(`Task ${id} moved from ${from} to ${to}`);
}}
onDragStart={(id: string, position: number) => {
// Parameters are typed
console.log(`Started dragging task ${id} at position ${position}`);
}}
>
<Text>{task.title}</Text>
</SortableItem>
);
}

Performance Tips​

  1. Use React.memo for item components to prevent unnecessary re-renders
  2. Memoize callback functions with useCallback
  3. Avoid complex calculations in render functions
  4. Use stable keys for consistent performance
// Good: Memoized component
const MemoizedSortableItem = React.memo(({ item, positions, ...props }) => (
<SortableItem id={item.id} positions={positions} {...props}>
<ItemContent item={item} />
</SortableItem>
));

// Good: Memoized callbacks
const handleMove = useCallback((id, from, to) => {
reorderItems(id, from, to);
}, []);

Accessibility​

The component includes accessibility features:

<SortableItem
id="task-1"
positions={positions}
{...props}
accessible={true}
accessibilityRole="button"
accessibilityLabel={`Reorder ${task.title}`}
accessibilityHint="Double tap and hold to drag"
>
<Text>{task.title}</Text>
</SortableItem>

See Also​