React Native Reanimated DnDReact Native Reanimated DnD
Context & Providers

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

  1. Minimize Context Reads: Cache context values when possible
  2. Throttle Updates: Limit frequency of position updates
  3. Selective Subscriptions: Only subscribe to needed context changes
  4. 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