Skip to main content

@nverk/react-native-dnd

Intuitive Drag and Drop for React Native, powered by Reanimated & Gesture Handler

Overview

@nverk/react-native-dnd is a lightweight and performant drag-and-drop solution for React Native applications. It leverages the power of react-native-gesture-handler for robust gesture recognition and react-native-reanimated (v3 compatible) for smooth, native-feeling animations.

Key Features:

  • Built with react-native-gesture-handler and react-native-reanimated.
  • Smooth animations and precise gesture handling.
  • Highly customizable drag and drop behavior.
  • Lightweight and focused API.
  • Type-safe with TypeScript.
  • Supports snapping, swapping, capacity limits, and various drop behaviors.

Installation

npm install @nverk/react-native-dnd
# or
yarn add @nverk/react-native-dnd

Peer Dependencies:

Ensure you have react-native-gesture-handler and react-native-reanimated installed and configured in your project.

  • react-native-gesture-handler >= 2.x.x
  • react-native-reanimated >= 3.x.x

Refer to their official installation guides for native setup:

Core Concept: DndProvider

All drag-and-drop functionality must be wrapped within a <DndProvider>. This component sets up the context for draggables and droppables to communicate.

import { DndProvider } from "@nverk/react-native-dnd";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { View, StyleSheet } from "react-native";

function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<DndProvider>
{/* Your app components that use drag and drop */}
</DndProvider>
</GestureHandlerRootView>
);
}

export default App;

Note: It's important to wrap your application with GestureHandlerRootView at the root, as shown in the example, for react-native-gesture-handler to work correctly.

Main Components

<Draggable>

Makes any child component draggable.

Props:

  • id: DndID (Required): A unique identifier for the draggable item.
  • style?: ViewStyle: Standard React Native style for the wrapping Animated.View.
  • children: ReactNode: The content to be made draggable.
  • onEnter?: (draggedId: DndID, droppableId: DndID | null) => void: Callback fired when this draggable enters a droppable area.
  • onLeave?: (draggedId: DndID, droppableId: DndID | null) => void: Callback fired when this draggable leaves a droppable area.
  • onDrop?: (draggedId: DndID, droppableId: DndID | null) => void: Callback fired when this draggable is dropped, whether on a droppable or not. droppableId will be null if not dropped on a valid target.
  • dropBehavior?: DraggableDropBehaviorType: Defines behavior when dropped outside a valid droppable.
    • 'snapToHome' (Default): Returns to its original starting position.
    • 'freeRoam': Stays at the drop location.
  • userAnimatedStyle?: (isDragging: boolean) => Record<string, unknown>: A Reanimated worklet function that returns animated styles based on the dragging state.
    // Example:
    const animatedStyle = (isDragging) => {
    "worklet";
    return {
    opacity: isDragging ? 0.7 : 1,
    transform: [{ scale: isDragging ? 1.1 : 1 }],
    };
    };
  • onDragStateChange?: (isDragging: boolean) => void: Callback when the dragging state (isDragging) of this item changes.
  • springConfig?: WithSpringConfig: Reanimated withSpring configuration for animations (e.g., snapping back to home or to a droppable).

Basic Usage Example:

import { Draggable } from "@nverk/react-native-dnd";
import Animated, {
useAnimatedStyle,
withTiming,
} from "react-native-reanimated";
import { View, Text, StyleSheet } from "react-native";

const MyDraggableItem = ({ id }) => {
const customAnimatedStyle = (isDragging) => {
"worklet";
return {
opacity: withTiming(isDragging ? 0.7 : 1),
transform: [{ scale: withTiming(isDragging ? 1.1 : 1) }],
};
};

return (
<Draggable
id={id}
userAnimatedStyle={customAnimatedStyle}
onDrop={(draggedId, droppableId) =>
console.log(`Item ${draggedId} dropped on ${droppableId}`)
}
>
<Animated.View style={styles.draggableBox}>
<Text>Drag Me ({id})</Text>
</Animated.View>
</Draggable>
);
};

const styles = StyleSheet.create({
draggableBox: {
width: 100,
height: 100,
backgroundColor: "skyblue",
justifyContent: "center",
alignItems: "center",
borderRadius: 10,
},
});

<Droppable>

Creates an area where <Draggable> components can be dropped.

Props:

  • id: DndID (Required): A unique identifier for the droppable area.
  • style?: ViewStyle: Standard React Native style for the wrapping Animated.View.
  • children: ReactNode: Content of the droppable area. Can be used to display items currently dropped in it or placeholder content.
  • onEnter?: (droppableId: DndID, draggedId: DndID | null) => void: Callback fired when a draggable enters this droppable area.
  • onLeave?: (droppableId: DndID, draggedId: DndID | null) => void: Callback fired when a draggable leaves this droppable area.
  • onDrop?: (droppableId: DndID, draggedId: DndID | null) => void: Callback fired when a draggable is dropped onto this droppable area.
  • capacity?: number: (Default: 1) Maximum number of draggables this droppable can hold.
  • swappable?: boolean: (Default: true) If true and capacity is reached, dropping a new item will attempt to displace an existing item (the oldest one dropped). If false and capacity is reached, new items cannot be dropped.
  • snapBehavior?: SnapBehaviorType: Defines how a dropped draggable snaps to this droppable.
    • 'center' (Default): Draggable snaps to the center of the droppable.
    • 'topLeft', 'topCenter', 'topRight', 'middleLeft', 'middleRight', 'bottomLeft', 'bottomCenter', 'bottomRight': Draggable snaps to the respective corner/edge.
    • 'none': Draggable stays where it was dropped within the droppable boundaries (no snapping).
    • { x: number, y: number }: Custom offset from the droppable's top-left corner for the draggable's top-left corner.
  • userAnimatedStyle?: (isHovered: boolean) => Record<string, unknown>: A Reanimated worklet function that returns animated styles based on whether a draggable is hovering over this droppable.
    // Example:
    const animatedStyle = (isHovered) => {
    "worklet";
    return {
    backgroundColor: isHovered ? "lightgreen" : "lightgrey",
    };
    };
  • onHoverStateChange?: (isHovered: boolean) => void: Callback when the hover state (isHovered) of this droppable changes.

Basic Usage Example:

import { Droppable } from "@nverk/react-native-dnd";
import Animated, {
useAnimatedStyle,
withTiming,
} from "react-native-reanimated";
import { View, Text, StyleSheet } from "react-native";

const MyDroppableArea = ({ id }) => {
const customAnimatedStyle = (isHovered) => {
"worklet";
return {
backgroundColor: withTiming(isHovered ? "lightblue" : "lightgrey"),
borderColor: withTiming(isHovered ? "blue" : "grey"),
borderWidth: 2,
};
};
return (
<Droppable
id={id}
style={styles.droppableArea}
userAnimatedStyle={customAnimatedStyle}
>
<Text>Drop Here ({id})</Text>
</Droppable>
);
};

const styles = StyleSheet.create({
droppableArea: {
width: 200,
height: 200,
padding: 10,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
},
});

Hooks

useDndContext()

Provides access to the internal state and methods of the DND system. This is useful for advanced scenarios or building custom components that interact with the DND state.

Return Value:

  • currentDraggableId: SharedValue<DndID | null>: The ID of the draggable currently being dragged, or null.
  • currentDroppableId: SharedValue<DndID | null>: The ID of the droppable currently being hovered over by a draggable, or null.
  • draggables: SharedValue<Record<DndID, DraggableType>>: A shared value record of all registered draggable items and their states.
  • droppables: SharedValue<Record<DndID, DroppableType>>: A shared value record of all registered droppable areas and their states.
  • droppedItems: SharedValue<Record<DndID, DndID>>: A shared value record mapping draggable IDs to the droppable ID they are currently occupying.
  • providerViewRef: AnimatedRef<View>: A ref to the main provider view, used for layout measurements.
  • requestResetItemPosition?: (itemId: DndID) => void: A worklet function to programmatically reset a draggable item to its home position.

Conceptual Example:

import { useDndContext } from "@nverk/react-native-dnd";
import { useAnimatedReaction, runOnJS } from "react-native-reanimated";
import { Button } from "react-native";

function DndMonitor() {
const { currentDraggableId, droppedItems, requestResetItemPosition } =
useDndContext();

useAnimatedReaction(
() => currentDraggableId.value,
(draggingId, _prevDraggingId) => {
if (draggingId) {
// runOnJS is needed if you want to call non-worklet functions like console.log
runOnJS(console.log)("Item currently being dragged:", draggingId);
}
},
[currentDraggableId] // Dependencies
);

const handleResetItem = (itemId) => {
if (requestResetItemPosition) {
// This needs to be called from a worklet context or wrapped with runOnUI if called from JS
// For simplicity, assuming this button is part of a component that can trigger JS -> UI thread
// In a real scenario, you might call this from a gesture handler or another UI-driven event.
// For direct JS call to worklet: runOnUI(requestResetItemPosition)(itemId);
console.log(`Requesting reset for item: ${itemId}`);
// A simple way if not in worklet context
// runOnUI(() => {
// requestResetItemPosition(itemId);
// })();
}
};

// Example: Button to reset a specific item (assuming 'item1' exists)
// return <Button title="Reset Item 1" onPress={() => handleResetItem('item1')} />;
return null; // Or render some monitoring UI
}

Key Types

  • DndID: string | number: Unique identifier for draggable and droppable components.
  • SnapBehaviorType: 'center' | 'topLeft' | 'topCenter' | 'topRight' | 'middleLeft' | 'middleRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'none' | { x: number; y: number }: Defines how a draggable snaps to a droppable.
  • DraggableDropBehaviorType: 'snapToHome' | 'freeRoam': Defines draggable behavior when not dropped on a target.
  • DraggableType: Internal representation of a draggable item's state and configuration.
    type DraggableType = {
    id: DndID;
    layout: { x: number; y: number; width: number; height: number }; // Relative to DndProvider
    callbacks?: DraggableCallbacks;
    start: { x: number; y: number }; // Initial offset when drag starts
    offset: { x: number; y: number }; // Current offset from original position
    dropBehavior?: DraggableDropBehaviorType;
    };
  • DroppableType: Internal representation of a droppable area's state and configuration.
    interface DroppableType {
    id: DndID;
    layout: { x: number; y: number; width: number; height: number }; // Relative to DndProvider
    callbacks?: DroppableCallbacks;
    capacity?: number;
    swappable?: boolean;
    snapBehavior?: SnapBehaviorType;
    }
  • DraggableCallbacks:
    type DraggableCallbacks = {
    onEnter?: (draggedId: DndID, droppableId: DndID | null) => void;
    onLeave?: (draggedId: DndID, droppableId: DndID | null) => void;
    onDrop?: (draggedId: DndID, droppableId: DndID | null) => void;
    };
  • DroppableCallbacks:
    type DroppableCallbacks = {
    onEnter?: (droppableId: DndID, draggedId: DndID | null) => void;
    onLeave?: (droppableId: DndID, draggedId: DndID | null) => void;
    onDrop?: (droppableId: DndID, draggedId: DndID | null) => void;
    };

Styling Dragging/Hover States

Utilize the userAnimatedStyle prop on both <Draggable> (receives isDragging) and <Droppable> (receives isHovered) to provide dynamic visual feedback. These functions are Reanimated worklets, allowing for direct manipulation of styles on the UI thread for maximum performance.

Capacity and Swapping

  • capacity on <Droppable>: Limits how many items can be in a droppable.
  • swappable on <Droppable>:
    • If true (default) and capacity is met, dropping a new item will remove the oldest item currently in the droppable (which will then respect its own dropBehavior, e.g., snap to home).
    • If false and capacity is met, no new items can be dropped until space is available.

Snap Behaviors (snapBehavior on <Droppable>)

  • Positional: 'center', 'topLeft', 'topRight', etc., align the draggable relative to the droppable.
  • 'none': The draggable remains at the exact position it was released within the droppable. The droppable still "captures" the item.
  • Custom { x: number, y: number }: Snaps the top-left of the draggable to this (x, y) offset relative to the top-left of the droppable.

Draggable Drop Behaviors (dropBehavior on <Draggable>)

This prop determines what happens to a draggable if it's released not over any valid droppable area.

  • 'snapToHome' (default): The draggable animates back to its original measured position.
  • 'freeRoam': The draggable remains at the location where it was dropped. Its "home" position for future drags effectively updates to this new spot.

Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

License

This library is licensed under the MIT License.