DropProvider
The DropProvider
component is the foundational context provider that enables drag-and-drop functionality throughout your application. It manages drop zone registration, collision detection, state tracking, and provides the necessary context for all draggable and droppable components.
Overview
The DropProvider creates a context that allows draggable and droppable components to communicate with each other. It handles:
- Drop Zone Registration: Manages all registered droppable areas
- Collision Detection: Determines when draggables intersect with droppables
- State Management: Tracks which items are dropped where
- Position Updates: Handles layout changes and position recalculations
- Capacity Management: Enforces drop zone capacity limits
- Global Callbacks: Provides application-wide drag event handling
Basic Usage
import { DropProvider } from 'react-native-reanimated-dnd';
import { Draggable, Droppable } from 'react-native-reanimated-dnd';
function App() {
return (
<DropProvider>
<View style={styles.container}>
<Draggable data={{ id: '1', name: 'Task 1' }}>
<View style={styles.draggableItem}>
<Text>Drag me!</Text>
</View>
</Draggable>
<Droppable onDrop={(data) => console.log('Dropped:', data)}>
<View style={styles.dropZone}>
<Text>Drop zone</Text>
</View>
</Droppable>
</View>
</DropProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
draggableItem: {
width: 100,
height: 50,
backgroundColor: '#3b82f6',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
dropZone: {
width: 200,
height: 100,
backgroundColor: '#f3f4f6',
borderRadius: 8,
borderWidth: 2,
borderStyle: 'dashed',
borderColor: '#d1d5db',
justifyContent: 'center',
alignItems: 'center',
marginTop: 50,
},
});
Props
DropProviderProps
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | Required | Child components that will have access to drag-and-drop context |
onLayoutUpdateComplete | () => void | - | Callback fired when layout updates are complete |
onDroppedItemsUpdate | (droppedItems: DroppedItemsMap) => void | - | Callback fired when dropped items mapping changes |
onDragging | (payload: DraggingPayload) => void | - | Global callback fired during drag operations |
onDragStart | (data: any) => void | - | Global callback fired when any drag operation starts |
onDragEnd | (data: any) => void | - | Global callback fired when any drag operation ends |
DraggingPayload
interface DraggingPayload {
x: number; // Original X position
y: number; // Original Y position
tx: number; // Current X translation
ty: number; // Current Y translation
itemData: any; // Data associated with the draggable item
}
DroppedItemsMap
interface DroppedItemsMap<TData = unknown> {
[draggableId: string]: {
droppableId: string;
data: TData;
};
}
Imperative Handle
The DropProvider exposes an imperative handle that provides methods for programmatic control:
DropProviderRef
interface DropProviderRef {
requestPositionUpdate: () => void;
getDroppedItems: () => DroppedItemsMap;
}
Method | Description |
---|---|
requestPositionUpdate() | Manually trigger position updates for all registered components |
getDroppedItems() | Get current mapping of dropped items |
Examples
Basic Setup with State Management
function TaskManager() {
const [tasks, setTasks] = useState([
{ id: '1', title: 'Design UI', status: 'todo' },
{ id: '2', title: 'Implement API', status: 'todo' },
{ id: '3', title: 'Write Tests', status: 'todo' },
]);
const [droppedItems, setDroppedItems] = useState({});
const handleDrop = (task, newStatus) => {
setTasks(prev => prev.map(t =>
t.id === task.id ? { ...t, status: newStatus } : t
));
};
return (
<DropProvider
onDroppedItemsUpdate={setDroppedItems}
onDragStart={(data) => {
console.log('Started dragging:', data.title);
hapticFeedback();
}}
onDragEnd={(data) => {
console.log('Finished dragging:', data.title);
}}
>
<View style={styles.container}>
<Text style={styles.title}>Task Manager</Text>
{/* Draggable Tasks */}
<View style={styles.tasksContainer}>
{tasks.filter(task => task.status === 'todo').map(task => (
<Draggable key={task.id} data={task}>
<TaskCard task={task} />
</Draggable>
))}
</View>
{/* Drop Zones */}
<View style={styles.columnsContainer}>
<Droppable
droppableId="in-progress"
onDrop={(task) => handleDrop(task, 'in-progress')}
>
<Column title="In Progress" tasks={tasks.filter(t => t.status === 'in-progress')} />
</Droppable>
<Droppable
droppableId="done"
onDrop={(task) => handleDrop(task, 'done')}
>
<Column title="Done" tasks={tasks.filter(t => t.status === 'done')} />
</Droppable>
</View>
</View>
</DropProvider>
);
}
Advanced Setup with Ref and Analytics
function AdvancedTaskBoard() {
const dropProviderRef = useRef<DropProviderRef>(null);
const [analytics, setAnalytics] = useState({
totalDrags: 0,
totalDrops: 0,
averageDragDuration: 0,
});
const dragStartTime = useRef<number>(0);
const handleLayoutChange = useCallback(() => {
// Trigger position update after layout changes
dropProviderRef.current?.requestPositionUpdate();
}, []);
const handleDragStart = useCallback((data) => {
dragStartTime.current = Date.now();
setAnalytics(prev => ({ ...prev, totalDrags: prev.totalDrags + 1 }));
// Global drag start logic
showDragOverlay(data);
updateGlobalDragState(true);
}, []);
const handleDragEnd = useCallback((data) => {
const duration = Date.now() - dragStartTime.current;
setAnalytics(prev => ({
...prev,
averageDragDuration: (prev.averageDragDuration + duration) / 2,
}));
// Global drag end logic
hideDragOverlay();
updateGlobalDragState(false);
}, []);
const handleDragging = useCallback(({ x, y, tx, ty, itemData }) => {
// Real-time position tracking
updateDragPosition(x + tx, y + ty);
// Update global drag indicator
updateGlobalDragIndicator({
position: { x: x + tx, y: y + ty },
item: itemData,
});
}, []);
const handleDroppedItemsUpdate = useCallback((droppedItems) => {
const dropCount = Object.keys(droppedItems).length;
setAnalytics(prev => ({ ...prev, totalDrops: dropCount }));
// Persist state
saveDroppedItemsToStorage(droppedItems);
// Update global state
updateGlobalDroppedItems(droppedItems);
}, []);
return (
<GestureHandlerRootView style={styles.container}>
<DropProvider
ref={dropProviderRef}
onLayoutUpdateComplete={() => {
console.log('Layout update complete');
// Trigger any additional UI updates
}}
onDroppedItemsUpdate={handleDroppedItemsUpdate}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragging={handleDragging}
>
<ScrollView
style={styles.scrollView}
onLayout={handleLayoutChange}
onContentSizeChange={handleLayoutChange}
>
<View style={styles.analyticsPanel}>
<Text>Total Drags: {analytics.totalDrags}</Text>
<Text>Total Drops: {analytics.totalDrops}</Text>
<Text>Avg Duration: {Math.round(analytics.averageDragDuration)}ms</Text>
</View>
<TaskBoard />
</ScrollView>
</DropProvider>
</GestureHandlerRootView>
);
}
Multi-Zone Setup with Capacity Limits
function FileOrganizer() {
const [files, setFiles] = useState(initialFiles);
const [folders, setFolders] = useState([
{ id: 'documents', name: 'Documents', maxFiles: 10 },
{ id: 'images', name: 'Images', maxFiles: 20 },
{ id: 'videos', name: 'Videos', maxFiles: 5 },
]);
const moveFileToFolder = useCallback((file, folderId) => {
setFiles(prev => prev.map(f =>
f.id === file.id ? { ...f, folderId } : f
));
}, []);
return (
<DropProvider
onDroppedItemsUpdate={(droppedItems) => {
// Update file locations based on drops
Object.entries(droppedItems).forEach(([fileId, { droppableId }]) => {
const file = files.find(f => f.id === fileId);
if (file && file.folderId !== droppableId) {
moveFileToFolder(file, droppableId);
}
});
}}
>
<View style={styles.organizer}>
{/* File List */}
<View style={styles.fileList}>
<Text style={styles.sectionTitle}>Files</Text>
{files.filter(file => !file.folderId).map(file => (
<Draggable key={file.id} data={file}>
<FileItem file={file} />
</Draggable>
))}
</View>
{/* Folder Drop Zones */}
<View style={styles.foldersContainer}>
{folders.map(folder => {
const folderFiles = files.filter(f => f.folderId === folder.id);
const isAtCapacity = folderFiles.length >= folder.maxFiles;
return (
<Droppable
key={folder.id}
droppableId={folder.id}
capacity={folder.maxFiles}
onDrop={(file) => {
if (!isAtCapacity) {
moveFileToFolder(file, folder.id);
showToast(`${file.name} moved to ${folder.name}`);
} else {
showError(`${folder.name} is full!`);
}
}}
activeStyle={{
backgroundColor: isAtCapacity ? '#fee2e2' : '#dcfce7',
borderColor: isAtCapacity ? '#ef4444' : '#22c55e',
}}
>
<FolderDropZone
folder={folder}
files={folderFiles}
isAtCapacity={isAtCapacity}
/>
</Droppable>
);
})}
</View>
</View>
</DropProvider>
);
}
Real-time Collaboration Setup
function CollaborativeBoard() {
const dropProviderRef = useRef<DropProviderRef>(null);
const [collaborators, setCollaborators] = useState([]);
const [realTimeUpdates, setRealTimeUpdates] = useState([]);
// WebSocket connection for real-time updates
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080/board');
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === 'ITEM_MOVED') {
// Update local state based on remote changes
handleRemoteItemMove(update.payload);
// Trigger position update to reflect changes
dropProviderRef.current?.requestPositionUpdate();
}
};
return () => ws.close();
}, []);
const handleDragStart = useCallback((data) => {
// Broadcast drag start to other users
broadcastToCollaborators({
type: 'DRAG_START',
userId: currentUser.id,
itemId: data.id,
timestamp: Date.now(),
});
}, []);
const handleDragging = useCallback(({ x, y, tx, ty, itemData }) => {
// Broadcast real-time position updates
throttledBroadcast({
type: 'DRAGGING',
userId: currentUser.id,
itemId: itemData.id,
position: { x: x + tx, y: y + ty },
timestamp: Date.now(),
});
}, []);
const handleDroppedItemsUpdate = useCallback((droppedItems) => {
// Broadcast final positions to other users
broadcastToCollaborators({
type: 'ITEMS_UPDATED',
userId: currentUser.id,
droppedItems,
timestamp: Date.now(),
});
// Save to backend
saveToBackend(droppedItems);
}, []);
return (
<DropProvider
ref={dropProviderRef}
onDragStart={handleDragStart}
onDragging={handleDragging}
onDragEnd={(data) => {
broadcastToCollaborators({
type: 'DRAG_END',
userId: currentUser.id,
itemId: data.id,
timestamp: Date.now(),
});
}}
onDroppedItemsUpdate={handleDroppedItemsUpdate}
>
<View style={styles.collaborativeBoard}>
{/* Show other users' cursors */}
{collaborators.map(collaborator => (
<CollaboratorCursor
key={collaborator.id}
collaborator={collaborator}
/>
))}
{/* Real-time update indicators */}
{realTimeUpdates.map(update => (
<UpdateIndicator key={update.id} update={update} />
))}
<BoardContent />
</View>
</DropProvider>
);
}
Context Integration
The DropProvider creates a context that can be accessed by child components:
import { useContext } from 'react';
import { SlotsContext } from 'react-native-reanimated-dnd';
function CustomComponent() {
const context = useContext(SlotsContext);
// Access context methods
const droppedItems = context.getDroppedItems();
const hasCapacity = context.hasAvailableCapacity('my-droppable');
return (
<View>
<Text>Dropped Items: {Object.keys(droppedItems).length}</Text>
<Text>Has Capacity: {hasCapacity ? 'Yes' : 'No'}</Text>
</View>
);
}
Performance Considerations
Optimization Tips
- Minimize Re-renders: Use
useCallback
for event handlers - Throttle Updates: Limit frequency of
onDragging
callbacks - Lazy Loading: Load drop zones only when needed
- Memory Management: Clean up listeners and refs properly
function OptimizedProvider({ children }) {
// Memoize callbacks to prevent unnecessary re-renders
const handleDragStart = useCallback((data) => {
// Optimized drag start logic
}, []);
const handleDragging = useMemo(
() => throttle(({ x, y, tx, ty, itemData }) => {
// Throttled dragging updates
}, 16), // 60fps
[]
);
return (
<DropProvider
onDragStart={handleDragStart}
onDragging={handleDragging}
>
{children}
</DropProvider>
);
}
Error Handling
function RobustProvider({ children }) {
const [error, setError] = useState(null);
const handleError = useCallback((error) => {
console.error('Drag and drop error:', error);
setError(error);
// Report to error tracking service
errorTracker.captureException(error);
}, []);
if (error) {
return <ErrorFallback error={error} onRetry={() => setError(null)} />;
}
return (
<ErrorBoundary onError={handleError}>
<DropProvider>
{children}
</DropProvider>
</ErrorBoundary>
);
}
TypeScript Support
The DropProvider is fully typed with generic support:
interface TaskData {
id: string;
title: string;
priority: 'low' | 'medium' | 'high';
}
function TypedTaskBoard() {
const handleDroppedItemsUpdate = useCallback(
(droppedItems: DroppedItemsMap<TaskData>) => {
// droppedItems is fully typed
Object.entries(droppedItems).forEach(([taskId, { data, droppableId }]) => {
// data is typed as TaskData
console.log(`Task ${data.title} dropped in ${droppableId}`);
});
},
[]
);
return (
<DropProvider onDroppedItemsUpdate={handleDroppedItemsUpdate}>
{/* Your components */}
</DropProvider>
);
}
Best Practices
- Single Provider: Use one DropProvider at the root of your drag-and-drop area
- Ref Management: Store the provider ref for programmatic control
- State Synchronization: Keep dropped items in sync with your app state
- Error Boundaries: Wrap the provider in error boundaries for robustness
- Performance Monitoring: Track drag performance and optimize as needed
See Also
- Draggable Component - Creating draggable items
- Droppable Component - Creating drop zones
- useDraggable Hook - Draggable hook API
- useDroppable Hook - Droppable hook API
- Basic Concepts - Understanding the fundamentals