API ReferenceHooks
useDraggable Hook
A powerful hook for creating draggable components with advanced features like collision detection, bounded dragging, axis constraints, and custom animations.
Overview
The useDraggable hook provides the core functionality for drag-and-drop interactions, handling gesture recognition, position tracking, collision detection with drop zones, and smooth animations. This is the underlying hook used by the Draggable component.
Import
import { useDraggable } from "react-native-reanimated-dnd";Parameters
UseDraggableOptions<TData>
Core Parameters
data
- Type:
TData - Required: Yes
- Description: Data payload associated with this draggable item. This data is passed to drop handlers when the item is successfully dropped.
const { animatedViewProps, gesture } = useDraggable({
data: { id: "1", title: "Task 1", priority: "high" },
});draggableId
- Type:
string - Required: No
- Description: Unique identifier for this draggable item. If not provided, one will be generated automatically.
dragDisabled
- Type:
boolean - Default:
false - Description: Whether dragging is disabled for this item. When true, the item cannot be dragged.
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
dragDisabled: !user.canDrag,
});preDragDelay
- Type:
number - Default:
0 - Description: Delay in milliseconds before dragging starts. Useful for preventing accidental drags or distinguishing between taps and drags.
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
preDragDelay: 200, // 200ms delay before drag activates
});Callback Parameters
onDragStart
- Type:
(data: TData) => void - Required: No
- Description: Callback fired when dragging starts.
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
onDragStart: (data) => {
console.log("Started dragging:", data.title);
hapticFeedback();
},
});onDragEnd
- Type:
(data: TData) => void - Required: No
- Description: Callback fired when dragging ends (regardless of whether it was dropped successfully).
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
onDragEnd: (data) => {
console.log("Finished dragging:", data.title);
setIsDragging(false);
},
});onDragging
- Type:
(payload: DraggingPayload<TData>) => void - Required: No
- Description: Callback fired continuously while dragging. Useful for real-time feedback.
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
onDragging: ({ x, y, tx, ty, itemData }) => {
const currentX = x + tx;
const currentY = y + ty;
console.log(`${itemData.title} is at (${currentX}, ${currentY})`);
},
});onStateChange
- Type:
(state: DraggableState) => void - Required: No
- Description: Callback fired when the draggable state changes.
const { animatedViewProps, gesture, state } = useDraggable({
data: taskData,
onStateChange: (state) => {
if (state === DraggableState.DROPPED) {
showSuccessMessage();
}
},
});Advanced Parameters
animationFunction
- Type:
AnimationFunction - Required: No
- Description: Custom animation function for controlling how the item animates when dropped. If not provided, uses default spring animation.
const bounceAnimation = (toValue) => {
"worklet";
return withTiming(toValue, {
duration: 600,
easing: Easing.bounce,
});
};
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
animationFunction: bounceAnimation,
});dragBoundsRef
- Type:
React.RefObject<Animated.View | View> - Required: No
- Description: Reference to a View that defines the dragging boundaries. The draggable item will be constrained within this view's bounds.
const boundsRef = useRef<View>(null);
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
dragBoundsRef: boundsRef,
});dragAxis
- Type:
"x" | "y" | "both" - Default:
"both" - Description: Constrains dragging to a specific axis.
// Horizontal only
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
dragAxis: "x",
});
// Vertical only
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
dragAxis: "y",
});collisionAlgorithm
- Type:
CollisionAlgorithm - Default:
"intersect" - Description: Algorithm used for collision detection with drop zones.
Available algorithms:
intersect: Collision when any part overlaps (default)center: Collision when center point is over droppablecontain: Collision when entire draggable is contained
const { animatedViewProps, gesture } = useDraggable({
data: taskData,
collisionAlgorithm: "center",
});Return Value
UseDraggableReturn
animatedViewProps
- Type:
{ style: AnimatedStyle<ViewStyle>; onLayout: (event: LayoutChangeEvent) => void; } - Description: Props to spread on the animated view that will be draggable. Contains the animated style and layout handler.
const { animatedViewProps, gesture } = useDraggable({ data: taskData });
return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Draggable content</Text>
</Animated.View>
</GestureDetector>
);gesture
- Type:
GestureType - Description: Gesture object to attach to GestureDetector for handling drag interactions. Only used when no handle is present (entire component is draggable).
state
- Type:
DraggableState - Description: Current state of the draggable item (IDLE, DRAGGING, or DROPPED).
const { animatedViewProps, gesture, state } = useDraggable({ data: taskData });
return (
<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[
animatedViewProps.style,
{ opacity: state === DraggableState.DRAGGING ? 0.7 : 1 },
]}
>
<Text>State: {state}</Text>
</Animated.View>
</GestureDetector>
);animatedViewRef
- Type:
ReturnType<typeof useAnimatedRef<Animated.View>> - Description: Animated ref for the draggable view. Used internally for measurements.
hasHandle
- Type:
boolean - Description: Whether this draggable has a handle component. When true, only the handle can initiate dragging. When false, the entire component is draggable.
registerHandle
- Type:
(registered: boolean) => void - Description: Callback for handle components to register/unregister themselves. Called with
truewhen a handle mounts,falsewhen it unmounts.
Usage Examples
Basic Draggable
import { useDraggable, DraggableState } from "react-native-reanimated-dnd";
import { GestureDetector } from "react-native-gesture-handler";
import Animated from "react-native-reanimated";
function BasicDraggable() {
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>
<Text>State: {state}</Text>
</Animated.View>
</GestureDetector>
);
}Bounded Draggable
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 });
},
collisionAlgorithm: "center",
});
return (
<View ref={boundsRef} style={styles.container}>
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Bounded horizontal draggable</Text>
</Animated.View>
</GestureDetector>
</View>
);
}State Tracking Draggable
function StatefulDraggable() {
const [dragState, setDragState] = useState(DraggableState.IDLE);
const [position, setPosition] = useState({ x: 0, y: 0 });
const { animatedViewProps, gesture } = useDraggable({
data: { id: "3", status: "active" },
onStateChange: setDragState,
onDragging: ({ x, y, tx, ty }) => {
setPosition({ x: x + tx, y: y + ty });
},
});
return (
<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[
animatedViewProps.style,
{ opacity: dragState === DraggableState.DRAGGING ? 0.7 : 1 },
]}
>
<Text>State: {dragState}</Text>
<Text>
Position: ({Math.round(position.x)}, {Math.round(position.y)})
</Text>
</Animated.View>
</GestureDetector>
);
}Custom Animation Draggable
import { withSpring, withTiming, Easing } from "react-native-reanimated";
function CustomAnimationDraggable() {
const customBounce = useCallback((toValue) => {
"worklet";
return withSpring(toValue, {
damping: 10,
stiffness: 100,
mass: 1,
});
}, []);
const { animatedViewProps, gesture } = useDraggable({
data: { id: "4", type: "bouncy" },
animationFunction: customBounce,
onDragStart: () => hapticFeedback(),
});
return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Bouncy draggable</Text>
</Animated.View>
</GestureDetector>
);
}Analytics Draggable
function AnalyticsDraggable() {
const [analytics, setAnalytics] = useState({
dragCount: 0,
totalDistance: 0,
averageDistance: 0,
});
const startPosition = useRef({ x: 0, y: 0 });
const { animatedViewProps, gesture } = useDraggable({
data: { id: "5", type: "analytics" },
onDragStart: (data) => {
startPosition.current = { x: 0, y: 0 }; // Will be updated in onDragging
setAnalytics((prev) => ({ ...prev, dragCount: prev.dragCount + 1 }));
},
onDragging: ({ x, y, tx, ty }) => {
if (startPosition.current.x === 0 && startPosition.current.y === 0) {
startPosition.current = { x, y };
}
},
onDragEnd: (data) => {
const distance = Math.sqrt(
Math.pow(startPosition.current.x, 2) +
Math.pow(startPosition.current.y, 2)
);
setAnalytics((prev) => {
const newTotal = prev.totalDistance + distance;
const newAverage = newTotal / prev.dragCount;
return {
...prev,
totalDistance: newTotal,
averageDistance: newAverage,
};
});
},
});
return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Analytics Draggable</Text>
<Text>Drags: {analytics.dragCount}</Text>
<Text>Avg Distance: {Math.round(analytics.averageDistance)}px</Text>
</Animated.View>
</GestureDetector>
);
}Collision Detection Example
function CollisionDraggable() {
const [collisionAlgorithm, setCollisionAlgorithm] = useState("intersect");
const { animatedViewProps, gesture } = useDraggable({
data: { id: "6", type: "collision-test" },
collisionAlgorithm,
onDragStart: () => {
console.log(`Using ${collisionAlgorithm} collision detection`);
},
});
return (
<View>
<View style={styles.controls}>
{["intersect", "center", "contain"].map((algorithm) => (
<TouchableOpacity
key={algorithm}
onPress={() => setCollisionAlgorithm(algorithm)}
style={[
styles.button,
collisionAlgorithm === algorithm && styles.activeButton,
]}
>
<Text>{algorithm}</Text>
</TouchableOpacity>
))}
</View>
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Collision: {collisionAlgorithm}</Text>
</Animated.View>
</GestureDetector>
</View>
);
}Conditional Dragging
function ConditionalDraggable({ item, userPermissions }) {
const canDrag = userPermissions.includes("drag") && !item.locked;
const { animatedViewProps, gesture } = useDraggable({
data: item,
dragDisabled: !canDrag,
onDragStart: (data) => {
if (data.sensitive && !userPermissions.includes("sensitive")) {
showError("Insufficient permissions");
return;
}
analytics.track("drag_start", { itemId: data.id });
},
});
return (
<GestureDetector gesture={gesture}>
<Animated.View
{...animatedViewProps}
style={[animatedViewProps.style, !canDrag && styles.disabled]}
>
<Text>{item.title}</Text>
{item.locked && <Icon name="lock" size={16} />}
{!canDrag && <Text style={styles.disabledText}>Drag disabled</Text>}
</Animated.View>
</GestureDetector>
);
}Real-time Position Tracking
function PositionTracker() {
const [realTimePosition, setRealTimePosition] = useState({ x: 0, y: 0 });
const [velocity, setVelocity] = useState({ x: 0, y: 0 });
const lastPosition = useRef({ x: 0, y: 0, timestamp: 0 });
const { animatedViewProps, gesture } = useDraggable({
data: { id: "7", type: "position-tracker" },
onDragging: ({ x, y, tx, ty }) => {
const currentX = x + tx;
const currentY = y + ty;
const now = Date.now();
setRealTimePosition({ x: currentX, y: currentY });
// Calculate velocity
if (lastPosition.current.timestamp > 0) {
const deltaTime = now - lastPosition.current.timestamp;
const deltaX = currentX - lastPosition.current.x;
const deltaY = currentY - lastPosition.current.y;
setVelocity({
x: (deltaX / deltaTime) * 1000, // pixels per second
y: (deltaY / deltaTime) * 1000,
});
}
lastPosition.current = { x: currentX, y: currentY, timestamp: now };
},
onDragEnd: () => {
setVelocity({ x: 0, y: 0 });
lastPosition.current = { x: 0, y: 0, timestamp: 0 };
},
});
return (
<View>
<View style={styles.info}>
<Text>
Position: ({Math.round(realTimePosition.x)},{" "}
{Math.round(realTimePosition.y)})
</Text>
<Text>
Velocity: ({Math.round(velocity.x)}, {Math.round(velocity.y)}) px/s
</Text>
</View>
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Position Tracker</Text>
</Animated.View>
</GestureDetector>
</View>
);
}TypeScript Support
The hook is fully typed with generic support:
interface TaskData {
id: string;
title: string;
priority: "low" | "medium" | "high";
assignee?: string;
}
function TypedDraggable() {
const { animatedViewProps, gesture, state } = useDraggable<TaskData>({
data: { id: "1", title: "Task 1", priority: "high" },
onDragStart: (data: TaskData) => {
// data is properly typed
console.log(`Started dragging: ${data.title} (${data.priority})`);
},
onDragEnd: (data: TaskData) => {
// data is properly typed
console.log(`Finished dragging: ${data.title}`);
},
});
return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>Typed draggable</Text>
</Animated.View>
</GestureDetector>
);
}Performance Tips
- Use
useCallbackfor event handlers to prevent unnecessary re-renders - Memoize expensive calculations with
useMemo - Throttle position updates in
onDraggingcallbacks - Use worklets for animation functions to run on the UI thread
- Avoid complex logic in drag callbacks for smooth performance
Common Patterns
Drag Handle Pattern
function DragHandlePattern() {
return (
<Draggable data={{ id: "1" }}>
<View>
<Text>Content (not draggable)</Text>
<Draggable.Handle>
<View style={styles.handle}>
<Text>Drag here</Text>
</View>
</Draggable.Handle>
</View>
</Draggable>
);
}State Machine Pattern
function StateMachineDraggable() {
const [state, setState] = useState(DraggableState.IDLE);
const { animatedViewProps, gesture } = useDraggable({
data: { id: "1" },
onStateChange: (newState) => {
// Validate state transitions
const validTransitions = {
[DraggableState.IDLE]: [DraggableState.DRAGGING],
[DraggableState.DRAGGING]: [
DraggableState.DROPPED,
DraggableState.IDLE,
],
[DraggableState.DROPPED]: [DraggableState.IDLE],
};
if (validTransitions[state].includes(newState)) {
setState(newState);
} else {
console.warn(`Invalid state transition: ${state} -> ${newState}`);
}
},
});
return (
<GestureDetector gesture={gesture}>
<Animated.View {...animatedViewProps}>
<Text>State Machine Draggable</Text>
<Text>Current State: {state}</Text>
</Animated.View>
</GestureDetector>
);
}See Also
- Draggable Component - High-level component using this hook
- DraggableState - State management
- CollisionAlgorithm - Collision detection
- AnimationFunction - Custom animations
- useDraggable Types - Complete type definitions