useSortable Hook
A hook for creating sortable list items with drag-and-drop reordering capabilities, position animations, and auto-scrolling support.
Overview
The useSortable
hook provides the core functionality for individual items within a sortable list, handling drag gestures, position animations, auto-scrolling, and reordering logic. It works in conjunction with useSortableList
to provide a complete sortable solution.
Import
import { useSortable } from 'react-native-reanimated-dnd';
Parameters
UseSortableOptions<T>
Core Parameters
id
- Type:
string
- Required: Yes
- Description: Unique identifier for this sortable item. Used for tracking position and reordering.
positions
- Type:
SharedValue<{ [id: string]: number }>
- Required: Yes
- Description: Shared value containing the position mapping for all items in the sortable list.
itemsCount
- Type:
number
- Required: Yes
- Description: Total number of items in the sortable list.
itemHeight
- Type:
number
- Required: Yes
- Description: Height of each item in pixels. Used for position calculations and animations.
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.
const { animatedStyle, panGestureHandler } = useSortable({
id: 'task-1',
positions: positionsSharedValue,
itemsCount: 10,
itemHeight: 60,
lowerBound: scrollBound,
autoScrollDirection: scrollDirection
});
Optional Parameters
containerHeight
- Type:
number
- Required: No
- Description: Height of the container holding the sortable list. Used for auto-scrolling calculations.
onMove
- Type:
(id: string, from: number, to: number) => void
- Required: No
- Description: Callback fired when the item is moved to a new position.
const { animatedStyle, panGestureHandler } = useSortable({
id: 'task-1',
positions,
itemsCount: 10,
itemHeight: 60,
lowerBound,
autoScrollDirection,
onMove: (id, from, to) => {
console.log(`Item ${id} moved from position ${from} to ${to}`);
reorderItems(id, from, to);
}
});
onDragStart
- Type:
(id: string, position: number) => void
- Required: No
- Description: Callback fired when dragging starts.
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.
children
- Type:
React.ReactNode
- Required: No
- Description: Used internally for handle detection. When provided, the hook will scan for handle components.
handleComponent
- Type:
React.ComponentType<any>
- Required: No
- Description: Component type to look for when detecting handles.
Return Value
UseSortableReturn
animatedStyle
- Type:
StyleProp<ViewStyle>
- Description: Animated styles to apply to the sortable item. Contains position and transformation animations.
const { animatedStyle, panGestureHandler } = useSortable(options);
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.item, animatedStyle]}>
<Text>Sortable content</Text>
</Animated.View>
</PanGestureHandler>
);
panGestureHandler
- Type:
any
- Description: Gesture handler props to spread on PanGestureHandler for handling drag interactions.
isMoving
- Type:
boolean
- Description: Whether this item is currently being dragged.
const { animatedStyle, panGestureHandler, isMoving } = useSortable(options);
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View
style={[
styles.item,
animatedStyle,
isMoving && styles.dragging
]}
>
<Text>Item content</Text>
</Animated.View>
</PanGestureHandler>
);
hasHandle
- Type:
boolean
- Description: Whether this sortable item has a handle component. When true, only the handle can initiate dragging.
Usage Examples
Basic Sortable Item
import { useSortable } from 'react-native-reanimated-dnd';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
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);
},
onDragStart: (id, position) => {
console.log(`Started dragging task ${id} at position ${position}`);
hapticFeedback();
}
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View
style={[
styles.taskItem,
animatedStyle,
isMoving && styles.dragging
]}
>
<Text style={[styles.taskText, isMoving && styles.draggingText]}>
{task.title}
</Text>
<Text style={styles.taskStatus}>
{task.completed ? 'Done' : 'Pending'}
</Text>
</Animated.View>
</PanGestureHandler>
);
}
const styles = StyleSheet.create({
taskItem: {
backgroundColor: '#fff',
padding: 16,
marginVertical: 4,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
dragging: {
opacity: 0.8,
transform: [{ scale: 1.05 }],
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
taskText: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
draggingText: {
color: '#007AFF',
},
taskStatus: {
fontSize: 14,
color: '#666',
},
});
Sortable Item with State Tracking
function AdvancedSortableItem({ task, positions, ...sortableProps }) {
const [dragState, setDragState] = useState('idle'); // idle, dragging, dropped
const [hoverTarget, setHoverTarget] = useState(null);
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: task.id,
positions,
...sortableProps,
onDragStart: (id, position) => {
setDragState('dragging');
hapticFeedback();
analytics.track('drag_start', { taskId: id, position });
},
onDrop: (id, position) => {
setDragState('dropped');
setTimeout(() => setDragState('idle'), 300);
analytics.track('drag_end', { taskId: id, position });
},
onDragging: (id, overItemId, yPosition) => {
setHoverTarget(overItemId);
if (overItemId) {
// Show visual feedback for item being hovered over
highlightItem(overItemId);
}
},
onMove: (id, from, to) => {
reorderTasks(id, from, to);
showToast(`Task moved to position ${to + 1}`);
}
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View
style={[
styles.taskItem,
animatedStyle,
styles[dragState]
]}
>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskPriority}>Priority: {task.priority}</Text>
{dragState === 'dragging' && (
<Text style={styles.dragIndicator}>
{hoverTarget ? `Over: ${hoverTarget}` : 'Dragging...'}
</Text>
)}
{dragState === 'dropped' && (
<Text style={styles.dropIndicator}>Dropped!</Text>
)}
</View>
<View style={styles.taskMeta}>
<Text style={styles.taskId}>#{task.id}</Text>
<Text style={styles.dragState}>{dragState}</Text>
</View>
</Animated.View>
</PanGestureHandler>
);
}
File List Sortable Item
interface FileItem {
id: string;
name: string;
size: number;
type: string;
lastModified: Date;
}
function SortableFileItem({ file, positions, ...sortableProps }) {
const [isSelected, setIsSelected] = useState(false);
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: file.id,
positions,
...sortableProps,
onMove: (id, from, to) => {
reorderFiles(id, from, to);
showToast(`${file.name} moved`);
}
});
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 (
<PanGestureHandler {...panGestureHandler}>
<Animated.View
style={[
styles.fileItem,
animatedStyle,
isSelected && styles.selectedFile,
isMoving && styles.movingFile
]}
>
<TouchableOpacity
onPress={() => setIsSelected(!isSelected)}
style={styles.fileContent}
>
<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>
<View style={styles.dragIndicator}>
<View style={styles.dragDots}>
<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>
</View>
</TouchableOpacity>
</Animated.View>
</PanGestureHandler>
);
}
Photo Gallery Sortable Item
function SortablePhotoItem({ photo, positions, ...sortableProps }) {
const [imageLoaded, setImageLoaded] = useState(false);
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: photo.id,
positions,
...sortableProps,
onMove: (id, from, to) => {
reorderPhotos(id, from, to);
},
onDragStart: () => {
hapticFeedback();
}
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View
style={[
styles.photoItem,
animatedStyle,
isMoving && styles.movingPhoto
]}
>
<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>
{isMoving && (
<View style={styles.movingIndicator}>
<Text style={styles.movingText}>Moving...</Text>
</View>
)}
</View>
</Animated.View>
</PanGestureHandler>
);
}
Custom Animated Sortable Item
function AnimatedSortableItem({ item, positions, ...sortableProps }) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const rotation = useSharedValue(0);
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: item.id,
positions,
...sortableProps,
onDragStart: () => {
scale.value = withSpring(1.1);
opacity.value = withTiming(0.9);
rotation.value = withSpring(2); // Slight rotation
},
onDrop: () => {
scale.value = withSpring(1);
opacity.value = withTiming(1);
rotation.value = withSpring(0);
}
});
const customAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` }
],
opacity: opacity.value,
};
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View
style={[
styles.item,
animatedStyle,
customAnimatedStyle
]}
>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.itemDescription}>{item.description}</Text>
</Animated.View>
</PanGestureHandler>
);
}
Performance Optimized Sortable Item
const MemoizedSortableItem = React.memo(({ item, positions, ...sortableProps }) => {
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: item.id,
positions,
...sortableProps,
onMove: useCallback((id, from, to) => {
reorderItems(id, from, to);
}, []),
onDragStart: useCallback((id, position) => {
hapticFeedback();
}, [])
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.item, animatedStyle]}>
<ItemContent item={item} isMoving={isMoving} />
</Animated.View>
</PanGestureHandler>
);
});
// Separate memoized content component
const ItemContent = React.memo(({ item, isMoving }) => (
<View style={[styles.content, isMoving && styles.movingContent]}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subtitle}>{item.subtitle}</Text>
</View>
));
Conditional Sortable Item
function ConditionalSortableItem({ item, positions, canReorder, ...sortableProps }) {
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: item.id,
positions,
...sortableProps,
onDragStart: (id, position) => {
if (!canReorder) {
showError('Reordering is disabled');
return;
}
hapticFeedback();
},
onMove: canReorder ? (id, from, to) => {
reorderItems(id, from, to);
} : undefined
});
return (
<PanGestureHandler
{...panGestureHandler}
enabled={canReorder}
>
<Animated.View
style={[
styles.item,
animatedStyle,
!canReorder && styles.disabled
]}
>
<Text style={styles.itemTitle}>{item.title}</Text>
{!canReorder && (
<Text style={styles.disabledText}>Reordering disabled</Text>
)}
{item.locked && <Icon name="lock" size={16} />}
</Animated.View>
</PanGestureHandler>
);
}
Real-time Position Tracking
function PositionTrackingSortableItem({ item, positions, ...sortableProps }) {
const [currentPosition, setCurrentPosition] = useState(0);
const [dragDistance, setDragDistance] = useState(0);
const startPosition = useRef(0);
const { animatedStyle, panGestureHandler, isMoving } = useSortable({
id: item.id,
positions,
...sortableProps,
onDragStart: (id, position) => {
startPosition.current = position;
setCurrentPosition(position);
},
onDragging: (id, overItemId, yPosition) => {
const distance = Math.abs(yPosition - (startPosition.current * sortableProps.itemHeight));
setDragDistance(distance);
},
onMove: (id, from, to) => {
setCurrentPosition(to);
reorderItems(id, from, to);
},
onDrop: () => {
setDragDistance(0);
}
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.item, animatedStyle]}>
<View style={styles.itemContent}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.positionInfo}>
Position: {currentPosition + 1}
</Text>
{isMoving && (
<Text style={styles.dragInfo}>
Drag distance: {Math.round(dragDistance)}px
</Text>
)}
</View>
</Animated.View>
</PanGestureHandler>
);
}
TypeScript Support
The hook is fully typed with generic support:
interface TaskData {
id: string;
title: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
}
function TypedSortableItem({ task, positions, ...props }) {
const { animatedStyle, panGestureHandler, isMoving } = useSortable<TaskData>({
id: task.id,
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}`);
}
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.item, animatedStyle]}>
<Text>{task.title}</Text>
</Animated.View>
</PanGestureHandler>
);
}
Performance Tips
- Use
React.memo
for item components to prevent unnecessary re-renders - Memoize callback functions with
useCallback
- Avoid complex calculations in render functions
- Use stable keys for consistent performance
- Throttle position updates for large lists
// Good: Memoized component and callbacks
const MemoizedSortableItem = React.memo(({ item, ...props }) => {
const handleMove = useCallback((id, from, to) => {
reorderItems(id, from, to);
}, []);
const { animatedStyle, panGestureHandler } = useSortable({
id: item.id,
...props,
onMove: handleMove
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.item, animatedStyle]}>
<ItemContent item={item} />
</Animated.View>
</PanGestureHandler>
);
});
Common Patterns
Handle Detection Pattern
function SortableWithHandle({ item, positions, ...props }) {
const { animatedStyle, panGestureHandler, hasHandle } = useSortable({
id: item.id,
positions,
...props,
children: (
<View>
<Text>{item.title}</Text>
<SortableHandle>
<Icon name="drag-handle" />
</SortableHandle>
</View>
),
handleComponent: SortableHandle
});
return (
<PanGestureHandler {...panGestureHandler}>
<Animated.View style={[styles.item, animatedStyle]}>
{hasHandle ? (
// Handle controls dragging
<View>
<Text>{item.title}</Text>
<SortableHandle>
<Icon name="drag-handle" />
</SortableHandle>
</View>
) : (
// Entire item is draggable
<Text>{item.title}</Text>
)}
</Animated.View>
</PanGestureHandler>
);
}
See Also
- SortableItem Component - High-level component using this hook
- useSortableList Hook - List management hook
- Sortable Component - Complete sortable list solution
- ScrollDirection Enum - Auto-scroll direction values
- UseSortableOptions Type - Complete type definitions