DragDropContext
The DragDropContext (exported as SlotsContext) is the React context that provides the underlying infrastructure for drag-and-drop functionality. It's automatically created by the DropProvider and consumed by draggable and droppable components throughout the library.
Overview
The DragDropContext manages:
- Drop Zone Registration: Tracks all registered droppable areas
- Active State Management: Monitors which drop zones are currently active
- Position Updates: Handles layout changes and position recalculations
- Dropped Items Tracking: Maintains state of which items are dropped where
- Capacity Management: Enforces drop zone capacity limits
- Event Coordination: Coordinates drag events across components
Context Value
SlotsContextValue Interface
interface SlotsContextValue<TData = unknown> {
// Drop zone management
register: (id: number, slot: DropSlot<TData>) => void;
unregister: (id: number) => void;
getSlots: () => Record<number, DropSlot<TData>>;
isRegistered: (id: number) => boolean;
// Active state management
setActiveHoverSlot: (id: number | null) => void;
activeHoverSlotId: number | null;
// Position updates
registerPositionUpdateListener: (
id: string,
listener: PositionUpdateListener
) => void;
unregisterPositionUpdateListener: (id: string) => void;
requestPositionUpdate: () => void;
// Dropped items management
registerDroppedItem: (
draggableId: string,
droppableId: string,
itemData: any
) => void;
unregisterDroppedItem: (draggableId: string) => void;
getDroppedItems: () => DroppedItemsMap<any>;
// Capacity management
hasAvailableCapacity: (droppableId: string) => boolean;
// Event callbacks
onDragging?: (payload: DraggingPayload) => void;
onDragStart?: (data: any) => void;
onDragEnd?: (data: any) => void;
}DropSlot Interface
interface DropSlot<TData = unknown> {
id: string;
x: number;
y: number;
width: number;
height: number;
onDrop: (data: TData) => void;
dropAlignment?: DropAlignment;
dropOffset?: DropOffset;
capacity?: number;
}Accessing the Context
Using useContext Hook
import { useContext } from "react";
import { SlotsContext } from "react-native-reanimated-dnd";
function CustomDragComponent() {
const context = useContext(SlotsContext);
if (!context) {
throw new Error("CustomDragComponent must be used within a DropProvider");
}
const { getSlots, getDroppedItems, hasAvailableCapacity, activeHoverSlotId } =
context;
return (
<View>
<Text>Active Slots: {Object.keys(getSlots()).length}</Text>
<Text>Dropped Items: {Object.keys(getDroppedItems()).length}</Text>
<Text>Active Hover: {activeHoverSlotId || "None"}</Text>
</View>
);
}Custom Hook for Context Access
import { useContext } from "react";
import { SlotsContext } from "react-native-reanimated-dnd";
function useDragDropContext() {
const context = useContext(SlotsContext);
if (!context) {
throw new Error("useDragDropContext must be used within a DropProvider");
}
return context;
}
// Usage
function MyComponent() {
const { getDroppedItems, hasAvailableCapacity } = useDragDropContext();
const droppedItems = getDroppedItems();
const canDrop = hasAvailableCapacity("my-drop-zone");
return (
<View>
<Text>Items: {Object.keys(droppedItems).length}</Text>
<Text>Can Drop: {canDrop ? "Yes" : "No"}</Text>
</View>
);
}Context Methods
Drop Zone Management
register(id, slot)
Registers a new drop zone with the context.
function CustomDroppable({ children, onDrop }) {
const context = useDragDropContext();
const viewRef = useRef<View>(null);
const slotId = useRef(Math.random());
useEffect(() => {
const measureAndRegister = () => {
viewRef.current?.measure((x, y, width, height, pageX, pageY) => {
context.register(slotId.current, {
id: "custom-droppable",
x: pageX,
y: pageY,
width,
height,
onDrop,
dropAlignment: "center",
capacity: 1,
});
});
};
measureAndRegister();
return () => context.unregister(slotId.current);
}, [context, onDrop]);
return (
<View ref={viewRef} onLayout={measureAndRegister}>
{children}
</View>
);
}unregister(id)
Removes a drop zone from the context.
useEffect(() => {
// Register drop zone
context.register(slotId, slotData);
// Cleanup on unmount
return () => {
context.unregister(slotId);
};
}, []);getSlots()
Returns all currently registered drop zones.
function DropZoneDebugger() {
const { getSlots } = useDragDropContext();
const slots = getSlots();
return (
<View style={styles.debugger}>
<Text style={styles.title}>Registered Drop Zones</Text>
{Object.entries(slots).map(([id, slot]) => (
<View key={id} style={styles.slotInfo}>
<Text>ID: {slot.id}</Text>
<Text>
Position: ({slot.x}, {slot.y})
</Text>
<Text>
Size: {slot.width}×{slot.height}
</Text>
<Text>Capacity: {slot.capacity || "Unlimited"}</Text>
</View>
))}
</View>
);
}Active State Management
setActiveHoverSlot(id)
Sets the currently active (hovered) drop zone.
function CustomDraggable({ data, children }) {
const { setActiveHoverSlot, getSlots } = useDragDropContext();
const handleDragMove = (x: number, y: number) => {
const slots = getSlots();
let activeSlot = null;
// Find which slot the draggable is over
Object.entries(slots).forEach(([slotId, slot]) => {
if (
x >= slot.x &&
x <= slot.x + slot.width &&
y >= slot.y &&
y <= slot.y + slot.height
) {
activeSlot = parseInt(slotId);
}
});
setActiveHoverSlot(activeSlot);
};
return (
<GestureDetector gesture={handleDragMove}>
{children}
</GestureDetector>
);
}Position Updates
registerPositionUpdateListener(id, listener)
Registers a listener for position updates.
function ResponsiveDroppable({ children }) {
const { registerPositionUpdateListener, unregisterPositionUpdateListener } =
useDragDropContext();
const listenerId = useRef(`listener-${Math.random()}`);
useEffect(() => {
const updatePosition = () => {
// Re-measure and update position
measureAndUpdatePosition();
};
registerPositionUpdateListener(listenerId.current, updatePosition);
return () => {
unregisterPositionUpdateListener(listenerId.current);
};
}, []);
return <View>{children}</View>;
}requestPositionUpdate()
Triggers position updates for all registered listeners.
function LayoutChangeHandler() {
const { requestPositionUpdate } = useDragDropContext();
const handleLayoutChange = useCallback(() => {
// Trigger position updates after layout changes
requestPositionUpdate();
}, [requestPositionUpdate]);
return <ScrollView onLayout={handleLayoutChange}>{/* Content */}</ScrollView>;
}Dropped Items Management
registerDroppedItem(draggableId, droppableId, itemData)
Records that an item has been dropped in a specific zone.
function CustomDropHandler() {
const { registerDroppedItem } = useDragDropContext();
const handleDrop = (draggableData, droppableId) => {
// Register the drop
registerDroppedItem(draggableData.id, droppableId, draggableData);
// Handle the drop in your app logic
moveItemToZone(draggableData, droppableId);
};
return <DropZone onDrop={handleDrop} />;
}getDroppedItems()
Returns the current mapping of dropped items.
function DroppedItemsDisplay() {
const { getDroppedItems } = useDragDropContext();
const droppedItems = getDroppedItems();
return (
<View style={styles.container}>
<Text style={styles.title}>Dropped Items</Text>
{Object.entries(droppedItems).map(
([draggableId, { droppableId, data }]) => (
<View key={draggableId} style={styles.item}>
<Text>Item: {data.name}</Text>
<Text>Zone: {droppableId}</Text>
</View>
)
)}
</View>
);
}Capacity Management
hasAvailableCapacity(droppableId)
Checks if a drop zone has available capacity.
function CapacityAwareDropZone({ droppableId, maxItems, children }) {
const { hasAvailableCapacity } = useDragDropContext();
const [canAcceptDrop, setCanAcceptDrop] = useState(true);
useEffect(() => {
const checkCapacity = () => {
const hasCapacity = hasAvailableCapacity(droppableId);
setCanAcceptDrop(hasCapacity);
};
// Check capacity periodically or on context changes
checkCapacity();
}, [droppableId, hasAvailableCapacity]);
return (
<View style={[styles.dropZone, !canAcceptDrop && styles.fullDropZone]}>
{children}
{!canAcceptDrop && <Text style={styles.fullText}>Zone Full</Text>}
</View>
);
}Advanced Usage Examples
Real-time Drop Zone Visualization
function DropZoneVisualizer() {
const { getSlots, activeHoverSlotId } = useDragDropContext();
const [slots, setSlots] = useState({});
useEffect(() => {
const updateSlots = () => {
setSlots(getSlots());
};
// Update slots periodically
const interval = setInterval(updateSlots, 100);
return () => clearInterval(interval);
}, [getSlots]);
return (
<View style={StyleSheet.absoluteFill} pointerEvents="none">
{Object.entries(slots).map(([id, slot]) => (
<View
key={id}
style={[
styles.slotOverlay,
{
left: slot.x,
top: slot.y,
width: slot.width,
height: slot.height,
},
parseInt(id) === activeHoverSlotId && styles.activeSlotOverlay,
]}
>
<Text style={styles.slotLabel}>{slot.id}</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
slotOverlay: {
position: "absolute",
borderWidth: 2,
borderColor: "rgba(59, 130, 246, 0.5)",
borderStyle: "dashed",
backgroundColor: "rgba(59, 130, 246, 0.1)",
},
activeSlotOverlay: {
borderColor: "rgba(34, 197, 94, 0.8)",
backgroundColor: "rgba(34, 197, 94, 0.2)",
},
slotLabel: {
fontSize: 10,
color: "#3b82f6",
fontWeight: "bold",
padding: 2,
},
});Context-Aware Analytics
function DragDropAnalytics() {
const context = useDragDropContext();
const [analytics, setAnalytics] = useState({
totalSlots: 0,
totalDroppedItems: 0,
activeSlots: 0,
capacityUtilization: 0,
});
useEffect(() => {
const updateAnalytics = () => {
const slots = context.getSlots();
const droppedItems = context.getDroppedItems();
const totalSlots = Object.keys(slots).length;
const totalDroppedItems = Object.keys(droppedItems).length;
const activeSlots = context.activeHoverSlotId ? 1 : 0;
// Calculate capacity utilization
const totalCapacity = Object.values(slots).reduce(
(sum, slot) => sum + (slot.capacity || 1),
0
);
const capacityUtilization =
totalCapacity > 0 ? (totalDroppedItems / totalCapacity) * 100 : 0;
setAnalytics({
totalSlots,
totalDroppedItems,
activeSlots,
capacityUtilization,
});
};
const interval = setInterval(updateAnalytics, 1000);
return () => clearInterval(interval);
}, [context]);
return (
<View style={styles.analyticsPanel}>
<Text style={styles.analyticsTitle}>Drag & Drop Analytics</Text>
<Text>Total Drop Zones: {analytics.totalSlots}</Text>
<Text>Dropped Items: {analytics.totalDroppedItems}</Text>
<Text>Active Zones: {analytics.activeSlots}</Text>
<Text>Capacity Usage: {analytics.capacityUtilization.toFixed(1)}%</Text>
</View>
);
}Custom Collision Detection
function CustomCollisionDetector() {
const { getSlots, setActiveHoverSlot } = useDragDropContext();
const detectCollision = useCallback(
(dragX: number, dragY: number, dragWidth: number, dragHeight: number) => {
const slots = getSlots();
let collisionSlotId = null;
Object.entries(slots).forEach(([id, slot]) => {
// Custom collision algorithm - center point must be within slot
const dragCenterX = dragX + dragWidth / 2;
const dragCenterY = dragY + dragHeight / 2;
if (
dragCenterX >= slot.x &&
dragCenterX <= slot.x + slot.width &&
dragCenterY >= slot.y &&
dragCenterY <= slot.y + slot.height
) {
collisionSlotId = parseInt(id);
}
});
setActiveHoverSlot(collisionSlotId);
return collisionSlotId;
},
[getSlots, setActiveHoverSlot]
);
return { detectCollision };
}Context State Synchronization
function ContextStateSyncer() {
const context = useDragDropContext();
const [globalState, setGlobalState] = useGlobalState();
// Sync dropped items with global state
useEffect(() => {
const syncDroppedItems = () => {
const droppedItems = context.getDroppedItems();
setGlobalState((prev) => ({
...prev,
droppedItems,
}));
};
const interval = setInterval(syncDroppedItems, 500);
return () => clearInterval(interval);
}, [context, setGlobalState]);
// Sync slots with global state
useEffect(() => {
const syncSlots = () => {
const slots = context.getSlots();
setGlobalState((prev) => ({
...prev,
availableSlots: Object.keys(slots).length,
activeSlot: context.activeHoverSlotId,
}));
};
const interval = setInterval(syncSlots, 1000);
return () => clearInterval(interval);
}, [context, setGlobalState]);
return null; // This component only handles synchronization
}Error Handling
Context Validation
function validateContext(
context: SlotsContextValue | null
): asserts context is SlotsContextValue {
if (!context) {
throw new Error(
"DragDropContext not found. Make sure your component is wrapped in a DropProvider."
);
}
}
function SafeContextConsumer() {
const context = useContext(SlotsContext);
try {
validateContext(context);
// Safe to use context methods
const slots = context.getSlots();
return <View>{/* Your component */}</View>;
} catch (error) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{error.message}</Text>
</View>
);
}
}Performance Considerations
- Minimize Context Reads: Cache context values when possible
- Throttle Updates: Limit frequency of position updates
- Selective Subscriptions: Only subscribe to needed context changes
- Memory Management: Clean up listeners and registrations
function OptimizedContextConsumer() {
const context = useDragDropContext();
// Cache frequently accessed values
const slots = useMemo(() => context.getSlots(), [context]);
const droppedItems = useMemo(() => context.getDroppedItems(), [context]);
// Throttle expensive operations
const throttledPositionUpdate = useMemo(
() => throttle(context.requestPositionUpdate, 100),
[context.requestPositionUpdate]
);
return <View>{/* Your component */}</View>;
}TypeScript Support
The context is fully typed with generic support:
interface CustomData {
id: string;
name: string;
type: "task" | "file" | "note";
}
function TypedContextConsumer() {
const context = useContext(SlotsContext) as SlotsContextValue<CustomData>;
const droppedItems = context.getDroppedItems();
// droppedItems is typed with CustomData
Object.entries(droppedItems).forEach(([id, { data }]) => {
// data is typed as CustomData
console.log(`${data.type}: ${data.name}`);
});
return <View />;
}See Also
- DropProvider - The provider component that creates this context
- useDraggable Hook - Hook that consumes this context
- useDroppable Hook - Hook that consumes this context
- Basic Concepts - Understanding the fundamentals