Skip to main content

useSortable

The useSortable hook provides the core functionality for individual items within a sortable list, handling drag gestures, position animations, auto-scrolling, and reordering logic.

Overview

This hook works in conjunction with useSortableList to provide a complete sortable solution. It handles the drag interactions for individual items, manages their position within the list, and provides smooth animations during reordering.

Basic Usage

import { useSortable } from 'react-native-reanimated-dnd';
import { PanGestureHandler } from 'react-native-gesture-handler';

function SortableTaskItem({ task, positions, ...sortableProps }) {
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: task.id,
positions,
...sortableProps,
onMove: (id, from, to) => {
console.log(`Task ${id} moved from ${from} to ${to}`);
reorderTasks(id, from, to);
}
});

return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.taskItem, animatedStyle]}>
<Text style={[styles.taskText, isMoving && styles.dragging]}>
{task.title}
</Text>
</Animated.View>
</PanGestureHandler>
);
}

Parameters

UseSortableOptions<T>

OptionTypeDefaultDescription
idstringRequiredUnique identifier for the sortable item
positionsSharedValue<{[id: string]: number}>RequiredShared value tracking item positions
lowerBoundSharedValue<number>RequiredLower scroll boundary for auto-scroll
autoScrollDirectionSharedValue<ScrollDirection>RequiredAuto-scroll direction state
itemsCountnumberRequiredTotal number of items in the list
itemHeightnumberRequiredHeight of each item in pixels
containerHeightnumber-Height of the container
onMove(id: string, from: number, to: number) => void-Callback when item is moved
onDragStart(id: string, position: number) => void-Callback when dragging starts
onDrop(id: string, position: number) => void-Callback when dragging ends
onDragging(id: string, overItemId: string | null, yPosition: number) => void-Callback during dragging
childrenReactNode-Children for handle detection
handleComponentComponentType<any>-Handle component type

ScrollDirection Enum

enum ScrollDirection {
None = "none",
Up = "up",
Down = "down"
}

Return Value

UseSortableReturn

PropertyTypeDescription
animatedStyleStyleProp<ViewStyle>Animated styles for position and transforms
panGestureHandlerobjectPan gesture handler props
isMovingbooleanWhether the item is currently being dragged
hasHandlebooleanWhether the item has a drag handle

Examples

Basic Sortable Item with Callbacks

function TaskItem({ task, positions, ...sortableProps }) {
const [isDragging, setIsDragging] = useState(false);
const [dragStartTime, setDragStartTime] = useState(0);

const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: task.id,
positions,
...sortableProps,
onMove: (id, from, to) => {
console.log(`Task ${id} moved from position ${from} to ${to}`);

// Update data
reorderTasks(id, from, to);

// Analytics
analytics.track('task_reordered', {
taskId: id,
fromPosition: from,
toPosition: to,
taskTitle: task.title,
dragDuration: Date.now() - dragStartTime,
});

// Auto-save
saveTasks();
},
onDragStart: (id, position) => {
setIsDragging(true);
setDragStartTime(Date.now());
hapticFeedback();

console.log(`Started dragging task ${id} at position ${position}`);

// Show global drag feedback
showDragOverlay(task);
},
onDrop: (id, position) => {
setIsDragging(false);
hideDragOverlay();

console.log(`Dropped task ${id} at position ${position}`);

// Success feedback
showToast('Task reordered successfully');
},
onDragging: (id, overItemId, yPosition) => {
if (overItemId) {
// Highlight the item being hovered over
highlightItem(overItemId);
}

// Update drag position for global overlay
updateDragPosition(yPosition);
},
});

return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.taskItem, animatedStyle]}>
<View style={[
styles.taskContent,
isDragging && styles.draggingContent,
isMoving && styles.movingContent
]}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskDescription}>{task.description}</Text>
<View style={styles.taskMeta}>
<Text style={styles.taskPriority}>{task.priority}</Text>
<Text style={styles.taskDue}>{task.dueDate}</Text>
</View>
{isDragging && (
<View style={styles.dragIndicator}>
<Text style={styles.dragText}>Dragging...</Text>
</View>
)}
</View>
</Animated.View>
</PanGestureHandler>
);
}

const styles = StyleSheet.create({
taskItem: {
marginHorizontal: 16,
marginVertical: 4,
},
taskContent: {
backgroundColor: 'white',
borderRadius: 8,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
draggingContent: {
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
backgroundColor: '#f8f9fa',
},
movingContent: {
opacity: 0.8,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
color: '#1f2937',
},
taskDescription: {
fontSize: 14,
color: '#6b7280',
marginBottom: 8,
},
taskMeta: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
taskPriority: {
fontSize: 12,
fontWeight: 'bold',
textTransform: 'uppercase',
color: '#ef4444',
},
taskDue: {
fontSize: 12,
color: '#9ca3af',
},
dragIndicator: {
position: 'absolute',
top: 8,
right: 8,
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderRadius: 4,
padding: 4,
},
dragText: {
fontSize: 10,
color: '#3b82f6',
fontWeight: 'bold',
},
});

Sortable Item with Drag Handle

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

function TaskWithHandle({ task, positions, ...sortableProps }) {
const { animatedStyle, panGestureHandler, hasHandle, isMoving } = useSortable({
id: task.id,
positions,
...sortableProps,
children: (
<View style={styles.taskContent}>
<View style={styles.taskInfo}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskSubtitle}>{task.subtitle}</Text>
</View>
<SortableHandle 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>
</SortableHandle>
</View>
),
});

return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.taskContainer, animatedStyle]}>
<View style={[
styles.taskContent,
isMoving && styles.movingTask
]}>
<View style={styles.taskInfo}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskSubtitle}>{task.subtitle}</Text>
</View>

{/* Only this handle can initiate dragging */}
<SortableHandle 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>
</SortableHandle>
</View>
</Animated.View>
</PanGestureHandler>
);
}

const styles = StyleSheet.create({
taskContainer: {
marginHorizontal: 16,
marginVertical: 4,
},
taskContent: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
movingTask: {
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
transform: [{ scale: 1.02 }],
},
taskInfo: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
color: '#1f2937',
},
taskSubtitle: {
fontSize: 14,
color: '#6b7280',
},
dragHandle: {
padding: 8,
marginLeft: 12,
},
handleIcon: {
width: 20,
height: 20,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignContent: 'space-between',
},
handleDot: {
width: 3,
height: 3,
backgroundColor: '#9ca3af',
borderRadius: 1.5,
},
});

Advanced Sortable Item with State Management

function AdvancedSortableItem({ item, positions, ...sortableProps }) {
const [localState, setLocalState] = useState({
isDragging: false,
isHovered: false,
dragStartPosition: null,
dragDistance: 0,
});

const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: item.id,
positions,
...sortableProps,
onMove: (id, from, to) => {
// Calculate drag distance
const distance = Math.abs(to - from);
setLocalState(prev => ({ ...prev, dragDistance: distance }));

// Update global state
dispatch(reorderItems({ id, from, to }));

// Complex analytics
analytics.track('item_reordered', {
itemId: id,
fromPosition: from,
toPosition: to,
dragDistance: distance,
itemType: item.type,
category: item.category,
timestamp: Date.now(),
});

// Conditional auto-save based on distance
if (distance > 2) {
debouncedSave();
} else {
immediateSave();
}
},
onDragStart: (id, position) => {
setLocalState(prev => ({
...prev,
isDragging: true,
dragStartPosition: position,
dragDistance: 0,
}));

// Global state updates
dispatch(setDraggedItem(id));

// Haptic feedback based on item type
if (item.type === 'important') {
strongHapticFeedback();
} else {
lightHapticFeedback();
}
},
onDrop: (id, position) => {
setLocalState(prev => ({
...prev,
isDragging: false,
dragStartPosition: null,
}));

// Clear global drag state
dispatch(clearDraggedItem());

// Success feedback with position info
const moved = localState.dragStartPosition !== position;
if (moved) {
showToast(`Moved ${item.name} to position ${position + 1}`);
}
},
onDragging: (id, overItemId, yPosition) => {
// Update hover state
setLocalState(prev => ({
...prev,
isHovered: !!overItemId,
}));

// Real-time position feedback
if (overItemId) {
showPositionIndicator(overItemId, yPosition);
}

// Update global drag position for overlays
dispatch(updateDragPosition({ itemId: id, yPosition }));
},
});

// Derived state for styling
const itemStyle = useMemo(() => {
const baseStyle = [styles.item, animatedStyle];

if (localState.isDragging) {
baseStyle.push(styles.dragging);
}

if (localState.isHovered) {
baseStyle.push(styles.hovered);
}

if (isMoving) {
baseStyle.push(styles.moving);
}

// Style based on item properties
if (item.priority === 'high') {
baseStyle.push(styles.highPriority);
}

return baseStyle;
}, [animatedStyle, localState, isMoving, item.priority]);

return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={itemStyle}>
<View style={styles.itemContent}>
<View style={styles.itemHeader}>
<Text style={styles.itemTitle}>{item.name}</Text>
{localState.isDragging && (
<View style={styles.dragBadge}>
<Text style={styles.dragBadgeText}>
{localState.dragDistance > 0 ? `+${localState.dragDistance}` : 'Dragging'}
</Text>
</View>
)}
</View>

<Text style={styles.itemDescription}>{item.description}</Text>

<View style={styles.itemFooter}>
<Text style={styles.itemCategory}>{item.category}</Text>
<Text style={styles.itemPosition}>
Position: {positions.value[item.id] + 1}
</Text>
</View>
</View>
</Animated.View>
</PanGestureHandler>
);
}

File List Sortable Item

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

const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: file.id,
positions,
...sortableProps,
onMove: (id, from, to) => {
reorderFiles(id, from, to);

// File-specific analytics
analytics.track('file_reordered', {
fileId: id,
fileName: file.name,
fileType: file.type,
fileSize: file.size,
fromPosition: from,
toPosition: to,
});
},
});

const getFileIcon = (type) => {
switch (type) {
case 'image': return 'image';
case 'video': return 'video';
case 'audio': return 'music';
case 'document': return 'file-text';
default: return 'file';
}
};

const getFileColor = (type) => {
switch (type) {
case 'image': return '#10b981';
case 'video': return '#3b82f6';
case 'audio': return '#8b5cf6';
case 'document': return '#f59e0b';
default: return '#6b7280';
}
};

return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.fileItem, animatedStyle]}>
<Pressable
style={[
styles.fileContent,
isSelected && styles.selectedFile,
isMoving && styles.movingFile
]}
onPress={() => setIsSelected(!isSelected)}
onLongPress={() => setShowPreview(true)}
>
<View style={styles.fileIcon}>
<Icon
name={getFileIcon(file.type)}
size={24}
color={getFileColor(file.type)}
/>
</View>

<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={1}>
{file.name}
</Text>
<View style={styles.fileDetails}>
<Text style={styles.fileSize}>
{formatFileSize(file.size)}
</Text>
<Text style={styles.fileDate}>
{formatDate(file.modifiedDate)}
</Text>
</View>
</View>

<View style={styles.fileActions}>
{isSelected && (
<View style={styles.selectedIndicator}>
<Icon name="check" size={16} color="#3b82f6" />
</View>
)}

<SortableHandle style={styles.dragHandle}>
<Icon name="grip-vertical" size={16} color="#9ca3af" />
</SortableHandle>
</View>
</Pressable>

{showPreview && (
<FilePreview
file={file}
onClose={() => setShowPreview(false)}
/>
)}
</Animated.View>
</PanGestureHandler>
);
}

const styles = StyleSheet.create({
fileItem: {
marginHorizontal: 16,
marginVertical: 2,
},
fileContent: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
padding: 12,
borderRadius: 6,
borderWidth: 1,
borderColor: '#e5e7eb',
},
selectedFile: {
backgroundColor: '#eff6ff',
borderColor: '#3b82f6',
},
movingFile: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 4,
},
fileIcon: {
marginRight: 12,
},
fileInfo: {
flex: 1,
},
fileName: {
fontSize: 14,
fontWeight: '500',
marginBottom: 2,
color: '#1f2937',
},
fileDetails: {
flexDirection: 'row',
gap: 8,
},
fileSize: {
fontSize: 12,
color: '#6b7280',
},
fileDate: {
fontSize: 12,
color: '#9ca3af',
},
fileActions: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
selectedIndicator: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: '#dbeafe',
justifyContent: 'center',
alignItems: 'center',
},
dragHandle: {
padding: 4,
},
});

Custom Animation Sortable Item

function AnimatedSortableItem({ item, positions, ...sortableProps }) {
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const opacity = useSharedValue(1);

const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: item.id,
positions,
...sortableProps,
onDragStart: () => {
// Custom drag start animation
scale.value = withSpring(1.05, { damping: 15 });
rotation.value = withSpring(2, { damping: 20 });
opacity.value = withTiming(0.9, { duration: 200 });
},
onDrop: () => {
// Custom drop animation
scale.value = withSpring(1, { damping: 15 });
rotation.value = withSpring(0, { damping: 20 });
opacity.value = withTiming(1, { duration: 200 });
},
});

const customAnimatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` },
],
opacity: opacity.value,
}));

return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[
styles.animatedItem,
animatedStyle,
customAnimatedStyle
]}>
<View style={styles.itemContent}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.itemSubtitle}>{item.subtitle}</Text>
{isMoving && (
<View style={styles.movingIndicator}>
<Text style={styles.movingText}>Moving...</Text>
</View>
)}
</View>
</Animated.View>
</PanGestureHandler>
);
}

Auto-scrolling

The hook automatically handles scrolling when dragging items near the edges:

  • Scroll Up: When dragging near the top of the container
  • Scroll Down: When dragging near the bottom of the container
  • Smooth Scrolling: Uses momentum-based scrolling for natural feel
  • Configurable Threshold: Auto-scroll triggers based on proximity to edges

Handle Detection

The hook automatically detects if a handle component is present:

// Without handle - entire item is draggable
const { hasHandle } = useSortable({
id: item.id,
// ... other props
});
// hasHandle will be false

// With handle - only handle is draggable
const { hasHandle } = useSortable({
id: item.id,
children: (
<View>
<Text>Content</Text>
<SortableHandle>
<Icon name="drag" />
</SortableHandle>
</View>
),
// ... other props
});
// hasHandle will be true

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

TypeScript Support

The hook is fully typed with generic support:

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

// Fully typed hook usage
const { animatedStyle, panGestureHandler } = useSortable<TaskData>({
id: task.id,
positions,
// ... other props
onMove: (id: string, from: number, to: number) => {
// Fully typed callback
reorderTasks(id, from, to);
},
});

Integration with useSortableList

This hook is designed to work with useSortableList:

function SortableList() {
const { getItemProps, ...listProps } = useSortableList({
data: items,
itemHeight: 60,
});

return (
<SortableListContainer {...listProps}>
{items.map((item, index) => {
const itemProps = getItemProps(item, index);
return (
<SortableItemComponent key={item.id} {...itemProps} />
);
})}
</SortableListContainer>
);
}

See Also