Skip to main content

useSortableList Hook

A hook for managing sortable lists with drag-and-drop reordering capabilities, position tracking, and auto-scrolling support.

Overview

The useSortableList hook provides the foundational state management and utilities needed to create sortable lists. It handles position tracking, scroll synchronization, auto-scrolling, and provides helper functions for individual sortable items.

Import

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

Parameters

UseSortableListOptions<TData>

data

  • Type: TData[] (where TData extends { id: string })
  • Required: Yes
  • Description: Array of data items to manage in the sortable list. Each item must have an id property.

itemHeight

  • Type: number
  • Required: Yes
  • Description: Height of each item in pixels. Used for position calculations and auto-scrolling.

itemKeyExtractor

  • Type: (item: TData, index: number) => 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.
const sortableProps = useSortableList({
data: tasks,
itemHeight: 60,
itemKeyExtractor: (item) => item.uuid // Use uuid instead of id
});

Return Value

UseSortableListReturn<TData>

positions

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

scrollY

  • Type: SharedValue<number>
  • Description: Shared value tracking the current scroll position.

autoScroll

  • Type: SharedValue<ScrollDirection>
  • Description: Shared value controlling auto-scroll direction during dragging.

scrollViewRef

  • Type: ReturnType<typeof useAnimatedRef<Animated.ScrollView>>
  • Description: Animated ref for the scroll view container.

dropProviderRef

  • Type: React.RefObject<DropProviderRef>
  • Description: Ref for the DropProvider context.

handleScroll

  • Type: any
  • Description: Scroll handler to attach to the ScrollView's onScroll prop.

handleScrollEnd

  • Type: () => void
  • Description: Handler for scroll end events. Attach to onScrollEndDrag and onMomentumScrollEnd.

contentHeight

  • Type: number
  • Description: Calculated height of the scroll view content based on item count and height.

getItemProps

  • Type: (item: TData, index: number) => { id: string; positions: SharedValue<{[id: string]: number}>; lowerBound: SharedValue<number>; autoScrollDirection: SharedValue<ScrollDirection>; itemsCount: number; itemHeight: number; }
  • Description: Function that returns core props needed for each sortable item. These props should be spread onto SortableItem components along with additional props like data, children, and callbacks.
const { getItemProps } = useSortableList({ data: tasks, itemHeight: 60 });

// For each item in your render
const itemProps = getItemProps(task, index);
// Returns: { id, positions, lowerBound, autoScrollDirection, itemsCount, itemHeight }

// Use with SortableItem
<SortableItem
{...itemProps}
data={task}
onMove={handleMove}
>
<TaskContent task={task} />
</SortableItem>

Usage Examples

Basic Sortable List

import { useSortableList } from 'react-native-reanimated-dnd';
import { SortableItem } from 'react-native-reanimated-dnd';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';

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 {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = useSortableList({
data: tasks,
itemHeight: 60,
});

return (
<GestureHandlerRootView style={styles.container}>
<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{tasks.map((task, index) => {
const itemProps = getItemProps(task, index);
return (
<SortableItem key={task.id} {...itemProps}>
<View style={styles.taskItem}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskStatus}>
{task.completed ? 'Done' : 'Pending'}
</Text>
</View>
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
scrollView: {
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 Reordering Logic

function ReorderableTaskList() {
const [tasks, setTasks] = useState(initialTasks);

const handleReorder = useCallback((id: string, from: number, to: number) => {
setTasks(prevTasks => {
const newTasks = [...prevTasks];
const [movedTask] = newTasks.splice(from, 1);
newTasks.splice(to, 0, movedTask);
return newTasks;
});

// Optional: Save to backend
saveTasks(newTasks);

// Optional: Analytics
analytics.track('task_reordered', {
taskId: id,
from,
to,
totalTasks: tasks.length
});
}, [tasks.length]);

const sortableProps = useSortableList({
data: tasks,
itemHeight: 80,
});

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = sortableProps;

return (
<GestureHandlerRootView style={styles.container}>
<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{tasks.map((task, index) => {
const itemProps = getItemProps(task, index);
return (
<SortableItem
key={task.id}
{...itemProps}
onMove={handleReorder}
onDragStart={(id, position) => {
hapticFeedback();
console.log(`Started dragging task ${id} at position ${position}`);
}}
onDrop={(id, position) => {
console.log(`Dropped task ${id} at position ${position}`);
}}
>
<TaskCard task={task} />
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

Custom Key Extractor

interface CustomItem {
uuid: string;
name: string;
order: number;
category: string;
}

function CustomSortableList() {
const [items, setItems] = useState<CustomItem[]>(data);

const sortableProps = useSortableList({
data: items,
itemHeight: 50,
itemKeyExtractor: (item) => item.uuid, // Use uuid instead of id
});

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = sortableProps;

return (
<GestureHandlerRootView style={styles.container}>
<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{items.map((item, index) => {
const itemProps = getItemProps(item, index);
return (
<SortableItem key={item.uuid} {...itemProps}>
<View style={styles.customItem}>
<Text style={styles.itemName}>{item.name}</Text>
<Text style={styles.itemCategory}>{item.category}</Text>
<Text style={styles.itemOrder}>Order: {item.order}</Text>
</View>
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

File Manager Sortable List

interface FileItem {
id: string;
name: string;
size: number;
type: 'file' | 'folder';
lastModified: Date;
}

function FileManagerList() {
const [files, setFiles] = useState<FileItem[]>(fileData);
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());

const handleFileReorder = useCallback((id: string, from: number, to: number) => {
setFiles(prevFiles => {
const newFiles = [...prevFiles];
const [movedFile] = newFiles.splice(from, 1);
newFiles.splice(to, 0, movedFile);
return newFiles;
});

showToast(`${files.find(f => f.id === id)?.name} moved`);
}, [files]);

const sortableProps = useSortableList({
data: files,
itemHeight: 60,
});

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = sortableProps;

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];
};

return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Files ({files.length})</Text>
<Text style={styles.selectedCount}>
{selectedFiles.size} selected
</Text>
</View>

<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{files.map((file, index) => {
const itemProps = getItemProps(file, index);
const isSelected = selectedFiles.has(file.id);

return (
<SortableItem
key={file.id}
{...itemProps}
onMove={handleFileReorder}
>
<TouchableOpacity
onPress={() => {
const newSelected = new Set(selectedFiles);
if (isSelected) {
newSelected.delete(file.id);
} else {
newSelected.add(file.id);
}
setSelectedFiles(newSelected);
}}
style={[styles.fileItem, isSelected && styles.selectedFile]}
>
<View style={styles.fileIcon}>
<Text style={styles.iconText}>
{file.type === 'folder' ? '📁' : '📄'}
</Text>
</View>

<View style={styles.fileInfo}>
<Text style={styles.fileName}>{file.name}</Text>
<Text style={styles.fileDetails}>
{file.type === 'file' && `${formatFileSize(file.size)}`}
{file.lastModified.toLocaleDateString()}
</Text>
</View>

<View style={styles.dragHandle}>
<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>
</View>
</TouchableOpacity>
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}
interface Photo {
id: string;
uri: string;
title: string;
width: number;
height: number;
}

function PhotoGalleryList() {
const [photos, setPhotos] = useState<Photo[]>(photoData);

const handlePhotoReorder = useCallback((id: string, from: number, to: number) => {
setPhotos(prevPhotos => {
const newPhotos = [...prevPhotos];
const [movedPhoto] = newPhotos.splice(from, 1);
newPhotos.splice(to, 0, movedPhoto);
return newPhotos;
});
}, []);

const sortableProps = useSortableList({
data: photos,
itemHeight: 120, // Taller items for photos
});

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = sortableProps;

return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.galleryHeader}>
<Text style={styles.galleryTitle}>Photo Gallery</Text>
<Text style={styles.photoCount}>{photos.length} photos</Text>
</View>

<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{photos.map((photo, index) => {
const itemProps = getItemProps(photo, index);

return (
<SortableItem
key={photo.id}
{...itemProps}
onMove={handlePhotoReorder}
onDragStart={() => hapticFeedback()}
>
<View style={styles.photoItem}>
<Image
source={{ uri: photo.uri }}
style={styles.photoThumbnail}
resizeMode="cover"
/>

<View style={styles.photoInfo}>
<Text style={styles.photoTitle}>{photo.title}</Text>
<Text style={styles.photoDimensions}>
{photo.width} × {photo.height}
</Text>
</View>

<View style={styles.photoActions}>
<TouchableOpacity onPress={() => editPhoto(photo.id)}>
<Icon name="edit" size={20} color="#007AFF" />
</TouchableOpacity>
<TouchableOpacity onPress={() => deletePhoto(photo.id)}>
<Icon name="trash" size={20} color="#FF3B30" />
</TouchableOpacity>
</View>
</View>
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

Performance Optimized Large List

function LargeSortableList() {
const [items, setItems] = useState(generateLargeDataset(1000));

// Memoize the reorder handler
const handleReorder = useCallback((id: string, from: number, to: number) => {
setItems(prevItems => {
const newItems = [...prevItems];
const [movedItem] = newItems.splice(from, 1);
newItems.splice(to, 0, movedItem);
return newItems;
});
}, []);

const sortableProps = useSortableList({
data: items,
itemHeight: 50, // Smaller items for performance
});

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = sortableProps;

// Memoize item rendering
const renderItem = useCallback((item: any, index: number) => {
const itemProps = getItemProps(item, index);

return (
<SortableItem
key={item.id}
{...itemProps}
onMove={handleReorder}
>
<MemoizedListItem item={item} />
</SortableItem>
);
}, [getItemProps, handleReorder]);

return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Large List ({items.length} items)</Text>
</View>

<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
removeClippedSubviews={true} // Performance optimization
>
{items.map(renderItem)}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

// 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>
));

Conditional Sortable List

function ConditionalSortableList({ canReorder, userRole }) {
const [tasks, setTasks] = useState(taskData);

const handleReorder = useCallback((id: string, from: number, to: number) => {
if (!canReorder) {
showError('Reordering is disabled');
return;
}

setTasks(prevTasks => {
const newTasks = [...prevTasks];
const [movedTask] = newTasks.splice(from, 1);
newTasks.splice(to, 0, movedTask);
return newTasks;
});
}, [canReorder]);

const sortableProps = useSortableList({
data: tasks,
itemHeight: 70,
});

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = sortableProps;

return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Tasks</Text>
<Text style={styles.permission}>
Role: {userRole} | Reorder: {canReorder ? 'Enabled' : 'Disabled'}
</Text>
</View>

<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{tasks.map((task, index) => {
const itemProps = getItemProps(task, index);

return (
<SortableItem
key={task.id}
{...itemProps}
onMove={canReorder ? handleReorder : undefined}
onDragStart={(id, position) => {
if (!canReorder) {
showError('Reordering is disabled');
return;
}
hapticFeedback();
}}
>
<View style={[
styles.taskItem,
!canReorder && styles.disabledItem
]}>
<Text style={styles.taskTitle}>{task.title}</Text>
{task.locked && <Icon name="lock" size={16} />}
{!canReorder && (
<Text style={styles.disabledText}>Reorder disabled</Text>
)}
</View>
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

TypeScript Support

The hook is fully typed with generic support:

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

function TypedSortableList() {
const [tasks, setTasks] = useState<TaskData[]>(taskData);

const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
getItemProps,
} = useSortableList<TaskData>({
data: tasks,
itemHeight: 60,
itemKeyExtractor: (item: TaskData) => item.id // Properly typed
});

return (
<GestureHandlerRootView style={styles.container}>
<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{tasks.map((task: TaskData, index: number) => {
const itemProps = getItemProps(task, index);
return (
<SortableItem key={task.id} {...itemProps}>
<Text>{task.title}</Text>
</SortableItem>
);
})}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

Performance Tips

  1. Use React.memo for item components to prevent unnecessary re-renders
  2. Memoize callback functions with useCallback
  3. Use stable key extractors for consistent performance
  4. Enable removeClippedSubviews for large lists
  5. Throttle scroll events appropriately
// Good: Memoized components and callbacks
const MemoizedSortableItem = React.memo(({ item, ...props }) => (
<SortableItem {...props}>
<ItemContent item={item} />
</SortableItem>
));

const handleReorder = useCallback((id, from, to) => {
reorderItems(id, from, to);
}, []);

Common Patterns

Container Component Pattern

function SortableListContainer({ children, ...sortableProps }) {
const {
scrollViewRef,
dropProviderRef,
handleScroll,
handleScrollEnd,
contentHeight,
} = sortableProps;

return (
<GestureHandlerRootView style={styles.container}>
<DropProvider ref={dropProviderRef}>
<Animated.ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={styles.scrollView}
contentContainerStyle={{ height: contentHeight }}
onScrollEndDrag={handleScrollEnd}
onMomentumScrollEnd={handleScrollEnd}
>
{children}
</Animated.ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}

// Usage
function MyList() {
const sortableProps = useSortableList({ data, itemHeight: 60 });

return (
<SortableListContainer {...sortableProps}>
{data.map((item, index) => (
<SortableItem key={item.id} {...sortableProps.getItemProps(item, index)}>
<ItemContent item={item} />
</SortableItem>
))}
</SortableListContainer>
);
}

See Also