react-optimized-dnd

Minimal, fast and ergonomic drag-and-drop for React. Optimized for large lists and dead simple to use.

GitHubnpm

Installation

npm install react-optimized-dnd

Trello-like board example

P

Person 1-1

Person 1-1 is a software engineer

P

Person 1-2

Person 1-2 is a software engineer

P

Person 1-3

Person 1-3 is a software engineer

P

Person 1-4

Person 1-4 is a software engineer

P

Person 1-5

Person 1-5 is a software engineer

P

Person 1-6

Person 1-6 is a software engineer

P

Person 2-1

Person 2-1 is a designer

P

Person 2-2

Person 2-2 is a designer

P

Person 2-3

Person 2-3 is a designer

P

Person 2-4

Person 2-4 is a designer

P

Person 2-5

Person 2-5 is a designer

P

Person 2-6

Person 2-6 is a designer

P

Person 2-7

Person 2-7 is a designer

P

Person 2-8

Person 2-8 is a designer

P

Person 3-1

Person 3-1 is a designer

P

Person 3-2

Person 3-2 is a designer

P

Person 3-3

Person 3-3 is a designer

P

Person 3-4

Person 3-4 is a designer

P

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.

Time: 30s
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&apos;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});