react-optimized-dnd
Minimal, fast and ergonomic drag-and-drop for React. Optimized for large lists and dead simple to use.
GitHubnpmInstallation
npm install react-optimized-dnd
Trello-like board example
Person 1-1
Person 1-1 is a software engineer
Person 1-2
Person 1-2 is a software engineer
Person 1-3
Person 1-3 is a software engineer
Person 1-4
Person 1-4 is a software engineer
Person 1-5
Person 1-5 is a software engineer
Person 1-6
Person 1-6 is a software engineer
Person 2-1
Person 2-1 is a designer
Person 2-2
Person 2-2 is a designer
Person 2-3
Person 2-3 is a designer
Person 2-4
Person 2-4 is a designer
Person 2-5
Person 2-5 is a designer
Person 2-6
Person 2-6 is a designer
Person 2-7
Person 2-7 is a designer
Person 2-8
Person 2-8 is a designer
Person 3-1
Person 3-1 is a designer
Person 3-2
Person 3-2 is a designer
Person 3-3
Person 3-3 is a designer
Person 3-4
Person 3-4 is a designer
Person 3-5
Person 3-5 is a designer
1import Card from "@/components/Card";
2import Column from "@/components/Column";
3import { useState } from "react";
4import { ReactOptimizedDndProvider } from "react-optimized-dnd";
5import { columnsData } from "@/lib/data";
6
7export default function App() {
8 const [columns, setColumns] = useState(columnsData);
9
10 return (
11 <div className="flex gap-2">
12 <ReactOptimizedDndProvider
13 onDragStart={() => {
14 console.log("drag start");
15 }}
16 onDragEnd={(state) => {
17 const draggingElementData = state.draggingElement.data;
18 const overElementData = state.overElement.data;
19 if (!draggingElementData || !overElementData) return;
20
21 if (overElementData.type === "card") {
22 const newColumns = [...columns];
23
24 const targetColumnIndex = overElementData.columnIndex;
25 const targetCardIndex = overElementData.index;
26
27 const draggingElementColumnIndex = draggingElementData.columnIndex;
28 const draggingElementIndex = draggingElementData.index;
29 const draggingItem =
30 newColumns[draggingElementColumnIndex][draggingElementIndex];
31
32 newColumns[draggingElementColumnIndex] = newColumns[
33 draggingElementColumnIndex
34 ].filter((_, index) => index !== draggingElementIndex);
35
36 newColumns[targetColumnIndex].splice(
37 targetCardIndex,
38 0,
39 draggingItem
40 );
41
42 setColumns(newColumns);
43 console.log("drag end");
44 return;
45 }
46
47 if (overElementData.type === "column") {
48 const newColumns = [...columns];
49
50 const targetColumnIndex = overElementData.index;
51
52 const draggingElementColumnIndex = draggingElementData.columnIndex;
53 const draggingElementIndex = draggingElementData.index;
54 const draggingItem =
55 newColumns[draggingElementColumnIndex][draggingElementIndex];
56
57 newColumns[draggingElementColumnIndex] = newColumns[
58 draggingElementColumnIndex
59 ].filter((_, index) => index !== draggingElementIndex);
60
61 newColumns[targetColumnIndex] = [
62 ...newColumns[targetColumnIndex],
63 draggingItem,
64 ];
65
66 setColumns(newColumns);
67 console.log("drag end");
68 return;
69 }
70 }}
71 onDragOver={() => {
72 console.log("drag over");
73 }}
74 >
75 {columns.map((column, columnIndex) => (
76 <Column key={columnIndex} index={columnIndex}>
77 {column.map((item, cardIndex) => {
78 return (
79 <Card
80 key={cardIndex}
81 index={cardIndex}
82 columnIndex={columnIndex}
83 title={item.title}
84 description={item.description}
85 color={item.color}
86 />
87 );
88 })}
89 </Column>
90 ))}
91 </ReactOptimizedDndProvider>
92 </div>
93 );
94}
95
Ordering Game Example
Order the numbers from 1 to 9 in ascending order.
1import { useState, useEffect, useRef, useCallback } from "react";
2import { ReactOptimizedDndProvider } from "react-optimized-dnd";
3import Confetti from "react-confetti";
4import { shuffleArray } from "@/lib/utils";
5import { cardColors } from "@/lib/data";
6import GameBoard from "@/components/GameBoard";
7
8export default function OrderingGame() {
9 const [gameSize, setGameSize] = useState(9);
10 const [gameTime, setGameTime] = useState(30);
11 const [cards, setCards] = useState<{id: string, value: number, color: string}[]>([]);
12 const [timeLeft, setTimeLeft] = useState(gameTime);
13 const [gameState, setGameState] = useState<'playing' | 'won' | 'lost'>('playing');
14 const timerRef = useRef<NodeJS.Timeout | null>(null);
15 const [showConfetti, setShowConfetti] = useState(false);
16
17 const generateShuffledNumbers = useCallback((size = gameSize) => {
18 const numbers = Array.from({ length: size }, (_, i) => i + 1);
19 return shuffleArray(numbers).map((num, idx) => ({
20 id: "num-" + num,
21 value: num,
22 color: cardColors[idx % cardColors.length],
23 }));
24 }, [gameSize]);
25
26 useEffect(() => {
27 setCards(generateShuffledNumbers(gameSize));
28 setTimeLeft(gameTime);
29 setGameState('playing');
30 }, [gameSize, gameTime, generateShuffledNumbers]);
31
32 useEffect(() => {
33 if (gameState !== 'playing') return;
34 timerRef.current = setInterval(() => {
35 setTimeLeft((t) => t - 1);
36 }, 1000);
37 return () => {
38 if (timerRef.current) clearInterval(timerRef.current);
39 };
40 }, [gameState]);
41
42 useEffect(() => {
43 if (timeLeft <= 0 && gameState === 'playing') {
44 setGameState('lost');
45 }
46 }, [timeLeft, gameState]);
47
48 useEffect(() => {
49 if (gameState === 'playing' && cards.length > 0 && cards.every((c, i) => c.value === i + 1)) {
50 setGameState('won');
51 }
52 }, [cards, gameState]);
53
54 useEffect(() => {
55 if (gameState === 'won') {
56 setShowConfetti(true);
57 const timeout = setTimeout(() => setShowConfetti(false), 10000);
58 return () => clearTimeout(timeout)
59 } else {
60 setShowConfetti(false);
61 }
62 }, [gameState]);
63
64 const handleReplay = () => {
65 setCards(generateShuffledNumbers(gameSize));
66 setTimeLeft(gameTime);
67 setGameState('playing');
68 setShowConfetti(false);
69 };
70
71 const handleDragEnd = (state: { draggingElement: { data: { index: number } }, overElement: { data: { index: number } } }) => {
72 const dragging = state.draggingElement.data;
73 const over = state.overElement.data;
74 if (!dragging || !over) return;
75 if (dragging.index === over.index) return;
76 const newCards = [...cards];
77 const [removed] = newCards.splice(dragging.index, 1);
78 newCards.splice(over.index, 0, removed);
79 setCards(newCards);
80 };
81
82 return (
83 <div className="w-full p-4 bg-gray-100 rounded-xl flex flex-col items-center">
84 <div className="flex gap-4 mb-4 w-full items-center justify-center">
85 <label className="flex items-center gap-2 font-mono text-sm">
86 Size:
87 <input
88 type="number"
89 min={2}
90 max={30}
91 value={gameSize}
92 onChange={e => {
93 const newSize = Math.max(2, Math.min(30, Number(e.target.value)));
94 setGameSize(newSize);
95 setCards(generateShuffledNumbers(newSize));
96 setTimeLeft(gameTime);
97 setGameState('playing');
98 }}
99 className="w-16 px-2 py-1 rounded border border-gray-300 text-center"
100 />
101 </label>
102 <p className="text-sm text-gray-500">
103 Order the numbers from 1 to {gameSize} in ascending order.
104 </p>
105 <label className="flex items-center gap-2 font-mono text-sm">
106 Time:
107 <input
108 type="number"
109 min={5}
110 max={600}
111 value={gameTime}
112 onChange={e => {
113 const newTime = Math.max(5, Math.min(600, Number(e.target.value)));
114 setGameTime(newTime);
115 setCards(generateShuffledNumbers(gameSize));
116 setTimeLeft(newTime);
117 setGameState('playing');
118 }}
119 className="w-20 px-2 py-1 rounded border border-gray-300 text-center"
120 />
121 s
122 </label>
123 </div>
124 <div className="flex items-center justify-between w-full mb-4">
125 <span className="font-mono text-lg">Time: {timeLeft}s</span>
126 <button
127 className="px-3 py-1 rounded bg-blue-500 text-white font-semibold hover:bg-blue-600 transition"
128 onClick={handleReplay}
129 >
130 Replay
131 </button>
132 </div>
133 <ReactOptimizedDndProvider onDragEnd={handleDragEnd}>
134 <GameBoard cards={cards} gameState={gameState} />
135 </ReactOptimizedDndProvider>
136 {showConfetti && (
137 <div className="fixed inset-0 pointer-events-none z-50">
138 <Confetti width={window.innerWidth} height={window.innerHeight} />
139 </div>
140 )}
141 {gameState === 'lost' && (
142 <div className="mt-4 text-red-500 font-bold">Time's up! Try again.</div>
143 )}
144 {gameState === 'won' && (
145 <div className="mt-4 text-green-600 font-bold">Congratulations! You ordered them all!</div>
146 )}
147 </div>
148 );
149}
150
Reference
<ReactOptimizedDndProvider />
Context provider for drag-and-drop. Wrap your draggable/droppable tree with this. Accepts optional callbacks:
1<ReactOptimizedDndProvider
2 onDragStart?: (state: IReactOptimizedDndComponentState) => void;
3 onDragEnd?: (state: IReactOptimizedDndComponentState) => void;
4 onDragOver?: (state: IReactOptimizedDndComponentState) => void;
5>
6 {/* children */}
7</ReactOptimizedDndProvider>
useDraggable
Hook to make an element draggable. Returns refs and state for drag handling.
1const { handleRef, deltaPos, isDragging } = useDraggable({
2 data?: any, // Data to associate with the draggable
3 dragThreshold?: number, // px before drag starts (default: 3)
4 touchDragDelay?: number // ms delay before drag starts on touch devices (default: 120)
5});
useDroppable
Hook to make an element a drop target. Returns refs and state for drop handling.
1const { droppableRef, isOver } = useDroppable({
2 data?: any // Data to associate with the droppable
3});