Skip to main content

useDraggable

The useDraggable hook provides the core functionality for creating draggable components with advanced features like collision detection, bounded dragging, axis constraints, and custom animations.

Overview

This hook handles gesture recognition, position tracking, collision detection with drop zones, and smooth animations. It's the foundation for the Draggable component and can be used to create custom draggable implementations.

Basic Usage

import { useDraggable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';

function MyDraggable() {
const { animatedViewProps, gesture, state } = useDraggable({
data: { id: '1', name: 'Draggable Item' },
onDragStart: (data) => console.log('Started dragging:', data.name),
onDragEnd: (data) => console.log('Finished dragging:', data.name),
});

return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Drag me!</Text>
</Animated.View>
</GestureDetector>
);
}

Parameters

UseDraggableOptions<TData>

OptionTypeDefaultDescription
dataTDataRequiredData payload passed to drop handlers
draggableIdstringauto-generatedUnique identifier for the draggable
dragDisabledbooleanfalseWhether dragging is disabled
onDragStart(data: TData) => void-Callback when dragging starts
onDragEnd(data: TData) => void-Callback when dragging ends
onDragging(payload) => void-Callback during dragging
onStateChange(state) => void-Callback when state changes
animationFunctionAnimationFunctionspringCustom animation function
dragBoundsRefRefObject<View>-Bounds for constraining dragging
dragAxis'x' | 'y' | 'both''both'Axis constraints
collisionAlgorithmCollisionAlgorithm'intersect'Collision detection method

onDragging Payload

The onDragging callback receives an object with:

PropertyTypeDescription
xnumberOriginal X position
ynumberOriginal Y position
txnumberCurrent X translation
tynumberCurrent Y translation
itemDataTDataAssociated data

Return Value

UseDraggableReturn

PropertyTypeDescription
animatedViewPropsobjectProps for the animated view
animatedViewProps.styleAnimatedStyleAnimated transform styles
animatedViewProps.onLayoutfunctionLayout handler for measurements
gestureGestureTypePan gesture for drag handling
stateDraggableStateCurrent draggable state
animatedViewRefAnimatedRefRef for the draggable view
hasHandlebooleanWhether a handle component is present

Examples

Basic Draggable with State Tracking

function StatefulDraggable() {
const [dragState, setDragState] = useState(DraggableState.IDLE);

const { animatedViewProps, gesture } = useDraggable({
data: { id: '1', type: 'task' },
onStateChange: setDragState,
onDragging: ({ x, y, tx, ty, itemData }) => {
const currentX = x + tx;
const currentY = y + ty;
console.log(`${itemData.type} at (${currentX}, ${currentY})`);
},
});

return (
<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[
animatedViewProps.style,
styles.draggable,
dragState === DraggableState.DRAGGING && styles.dragging
]}
>
<Text>State: {dragState}</Text>
</Animated.View>
</GestureDetector>
);
}

const styles = StyleSheet.create({
draggable: {
width: 100,
height: 100,
backgroundColor: '#3b82f6',
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
},
dragging: {
opacity: 0.8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
});

Bounded Draggable with Custom Animation

function BoundedDraggable() {
const boundsRef = useRef<View>(null);

const { animatedViewProps, gesture } = useDraggable({
data: { id: '2', type: 'bounded' },
dragBoundsRef: boundsRef,
dragAxis: 'x', // Only horizontal movement
animationFunction: (toValue) => {
'worklet';
return withTiming(toValue, {
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
});
},
collisionAlgorithm: 'center',
onDragStart: (data) => {
hapticFeedback();
console.log('Started dragging bounded item');
},
});

return (
<View ref={boundsRef} style={styles.container}>
<Text style={styles.label}>Horizontal Slider</Text>
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps} style={styles.slider}>
<Text style={styles.sliderText}></Text>
</Animated.View>
</GestureDetector>
</View>
);
}

const styles = StyleSheet.create({
container: {
height: 60,
backgroundColor: '#f0f0f0',
borderRadius: 30,
justifyContent: 'center',
paddingHorizontal: 20,
marginVertical: 20,
},
label: {
position: 'absolute',
top: -25,
left: 0,
fontSize: 14,
color: '#666',
},
slider: {
width: 40,
height: 40,
backgroundColor: '#007AFF',
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
},
sliderText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
});

Draggable with Collision Detection

function CollisionDraggable() {
const [collisionAlgorithm, setCollisionAlgorithm] = useState<CollisionAlgorithm>('intersect');
const [collisionCount, setCollisionCount] = useState(0);

const { animatedViewProps, gesture, state } = useDraggable({
data: {
id: '3',
name: 'Collision Test',
algorithm: collisionAlgorithm
},
collisionAlgorithm,
onDragStart: (data) => {
setCollisionCount(0);
console.log(`Started dragging with ${data.algorithm} collision`);
},
onDragging: ({ itemData }) => {
// This fires when hovering over droppables
setCollisionCount(prev => prev + 1);
},
onStateChange: (newState) => {
if (newState === DraggableState.DROPPED) {
showToast('Successfully dropped!');
}
},
});

return (
<View style={styles.container}>
<View style={styles.controls}>
<Text style={styles.label}>Collision Algorithm:</Text>
<Picker
selectedValue={collisionAlgorithm}
onValueChange={setCollisionAlgorithm}
style={styles.picker}
>
<Picker.Item label="Intersect (Easy)" value="intersect" />
<Picker.Item label="Center (Precise)" value="center" />
<Picker.Item label="Contain (Strict)" value="contain" />
</Picker>
</View>

<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[
animatedViewProps.style,
styles.draggable,
getAlgorithmStyle(collisionAlgorithm)
]}
>
<Text style={styles.algorithmText}>
{collisionAlgorithm.toUpperCase()}
</Text>
<Text style={styles.stateText}>
State: {state}
</Text>
<Text style={styles.collisionText}>
Collisions: {collisionCount}
</Text>
</Animated.View>
</GestureDetector>
</View>
);
}

function getAlgorithmStyle(algorithm: CollisionAlgorithm) {
switch (algorithm) {
case 'center':
return { backgroundColor: '#ef4444' }; // Red for precise
case 'contain':
return { backgroundColor: '#f59e0b' }; // Orange for strict
case 'intersect':
default:
return { backgroundColor: '#10b981' }; // Green for easy
}
}

Advanced Draggable with Analytics

function AnalyticsDraggable() {
const [dragMetrics, setDragMetrics] = useState({
dragCount: 0,
totalDistance: 0,
averageDuration: 0,
});
const dragStartTime = useRef<number>(0);
const lastPosition = useRef({ x: 0, y: 0 });

const { animatedViewProps, gesture } = useDraggable({
data: {
id: '4',
type: 'analytics',
metrics: dragMetrics
},
onDragStart: (data) => {
dragStartTime.current = Date.now();
lastPosition.current = { x: 0, y: 0 };

// Analytics
analytics.track('drag_start', {
itemId: data.id,
itemType: data.type,
timestamp: dragStartTime.current,
});
},
onDragging: ({ x, y, tx, ty, itemData }) => {
const currentX = x + tx;
const currentY = y + ty;
const distance = Math.sqrt(
Math.pow(currentX - lastPosition.current.x, 2) +
Math.pow(currentY - lastPosition.current.y, 2)
);

setDragMetrics(prev => ({
...prev,
totalDistance: prev.totalDistance + distance,
}));

lastPosition.current = { x: currentX, y: currentY };
},
onDragEnd: (data) => {
const duration = Date.now() - dragStartTime.current;

setDragMetrics(prev => {
const newDragCount = prev.dragCount + 1;
const newAverageDuration =
(prev.averageDuration * prev.dragCount + duration) / newDragCount;

return {
dragCount: newDragCount,
totalDistance: prev.totalDistance,
averageDuration: newAverageDuration,
};
});

// Analytics
analytics.track('drag_end', {
itemId: data.id,
duration,
distance: dragMetrics.totalDistance,
timestamp: Date.now(),
});
},
});

return (
<View style={styles.container}>
<View style={styles.metrics}>
<Text style={styles.metricText}>
Drags: {dragMetrics.dragCount}
</Text>
<Text style={styles.metricText}>
Distance: {Math.round(dragMetrics.totalDistance)}px
</Text>
<Text style={styles.metricText}>
Avg Duration: {Math.round(dragMetrics.averageDuration)}ms
</Text>
</View>

<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[animatedViewProps.style, styles.analyticsDraggable]}
>
<Text style={styles.title}>Analytics Draggable</Text>
<Text style={styles.subtitle}>Tracking your drags</Text>
</Animated.View>
</GestureDetector>
</View>
);
}

Custom Animation Functions

function CustomAnimationDraggable() {
const [animationType, setAnimationType] = useState<'spring' | 'timing' | 'bounce'>('spring');

const getAnimationFunction = (type: string): AnimationFunction => {
switch (type) {
case 'timing':
return (toValue) => {
'worklet';
return withTiming(toValue, {
duration: 400,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
};
case 'bounce':
return (toValue) => {
'worklet';
return withTiming(toValue, {
duration: 800,
easing: Easing.bounce,
});
};
case 'spring':
default:
return (toValue) => {
'worklet';
return withSpring(toValue, {
damping: 15,
stiffness: 150,
mass: 1,
});
};
}
};

const { animatedViewProps, gesture } = useDraggable({
data: { id: '5', animationType },
animationFunction: getAnimationFunction(animationType),
onDragEnd: () => {
console.log(`Dropped with ${animationType} animation`);
},
});

return (
<View style={styles.container}>
<View style={styles.animationControls}>
<Text style={styles.label}>Animation Type:</Text>
{['spring', 'timing', 'bounce'].map((type) => (
<Pressable
key={type}
style={[
styles.animationButton,
animationType === type && styles.activeButton
]}
onPress={() => setAnimationType(type as any)}
>
<Text style={[
styles.buttonText,
animationType === type && styles.activeButtonText
]}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</Text>
</Pressable>
))}
</View>

<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[animatedViewProps.style, styles.animationDraggable]}
>
<Text style={styles.animationText}>
{animationType.toUpperCase()}
</Text>
<Text style={styles.animationSubtext}>
Drag and release to see animation
</Text>
</Animated.View>
</GestureDetector>
</View>
);
}

Draggable States

The hook tracks three distinct states through the DraggableState enum:

enum DraggableState {
IDLE = "IDLE", // At rest position
DRAGGING = "DRAGGING", // Being actively dragged
DROPPED = "DROPPED" // Successfully dropped
}

State Transitions

  • IDLE → DRAGGING: When user starts dragging
  • DRAGGING → DROPPED: When successfully dropped on a droppable
  • DRAGGING → IDLE: When drag ends without successful drop
  • DROPPED → IDLE: When animation completes and item returns to position

Collision Algorithms

intersect (Default)

  • Use case: General drag-and-drop, easy dropping
  • Behavior: Collision detected when any part overlaps
  • Best for: File uploads, general item moving

center

  • Use case: Precise positioning, slot-based interfaces
  • Behavior: Collision detected when center point is over droppable
  • Best for: Games, precise placement, grid layouts

contain

  • Use case: Strict containment requirements
  • Behavior: Collision detected when entire item is within droppable
  • Best for: Folder systems, strict boundaries

Performance Tips

  • Use useCallback for stable callback references
  • Avoid heavy computations in onDragging callback
  • Consider debouncing analytics or logging in callbacks
  • Use React.memo for draggable content that doesn't change frequently

TypeScript Support

The hook is fully typed with generic support:

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

// Fully typed hook usage
const { animatedViewProps, gesture } = useDraggable<TaskData>({
data: taskData,
onDragStart: (data: TaskData) => {
// data is fully typed with TaskData properties
console.log(data.title, data.priority);
},
});

Context Integration

The hook automatically integrates with the drag-and-drop context when used within a DropProvider:

<DropProvider>
<CustomDraggableComponent />
</DropProvider>

See Also