Wonderland
Search…
An Easy to Use React DnD Sortable Component
I hope this is actually easy to use...

Target

  • Easy to use
  • Less code

Code

You can simply copy the following code to a dnd-sortable.tsx file, and use as a component.
1
import React, { FC, ReactNode, useCallback, useRef, useState } from "react";
2
import { useDrag, useDrop } from "react-dnd";
3
import type { XYCoord, Identifier } from "dnd-core";
4
import update from "immutability-helper";
5
6
export interface NodeProps {
7
id: any;
8
index: number;
9
node: ReactNode;
10
moveNode: (dragIndex: number, hoverIndex: number) => void;
11
}
12
13
interface DragItem {
14
index: number;
15
id: string;
16
type: string;
17
}
18
19
export const Node: FC<NodeProps> = ({ id, index, node, moveNode: moveNode }) => {
20
const ref = useRef<HTMLDivElement>(null);
21
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
22
accept: "item",
23
collect(monitor) {
24
return {
25
handlerId: monitor.getHandlerId(),
26
};
27
},
28
hover(item: DragItem, monitor) {
29
if (!ref.current) {
30
return;
31
}
32
const dragIndex = item.index;
33
const hoverIndex = index;
34
35
// Don't replace items with themselves
36
if (dragIndex === hoverIndex) {
37
return;
38
}
39
40
// Determine rectangle on screen
41
const hoverBoundingRect = ref.current?.getBoundingClientRect();
42
43
// Get vertical middle
44
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
45
46
// Determine mouse position
47
const clientOffset = monitor.getClientOffset();
48
49
// Get pixels to the top
50
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
51
52
// Only perform the move when the mouse has crossed half of the items' height
53
// When dragging downwards, only move when the cursor is below 50%
54
// When dragging upwards, only move when the cursor is above 50%
55
56
// Dragging downwards
57
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
58
return;
59
}
60
61
// Dragging upwards
62
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
63
return;
64
}
65
66
// Time to actually perform the action
67
moveNode(dragIndex, hoverIndex);
68
69
// Note: we're mutating the monitor item here!
70
// Generally it's better to avoid mutations,
71
// but it's good here for the sake of performance
72
// to avoid expensive index searches.
73
item.index = hoverIndex;
74
},
75
});
76
77
const [{ isDragging }, drag] = useDrag({
78
type: "item",
79
item: () => {
80
return { id, index };
81
},
82
collect: (monitor: any) => ({
83
isDragging: monitor.isDragging(),
84
}),
85
});
86
87
const opacity = isDragging ? 0.5 : 1;
88
drag(drop(ref));
89
return (
90
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
91
{node}
92
</div>
93
);
94
};
95
96
export interface Item {
97
id: number;
98
node: ReactNode;
99
}
100
101
export default function DndSortable({ nodeList }: { nodeList: Array<ReactNode> }) {
102
// Set id for every react node for react-dnd to use
103
const [itemList, setItemList] = useState(nodeList.map((node, index) => ({ id: index, node: node })));
104
105
const moveNode = useCallback((dragIndex: number, hoverIndex: number) => {
106
setItemList((prevNodes: Item[]) =>
107
update(prevNodes, {
108
$splice: [
109
[dragIndex, 1],
110
[hoverIndex, 0, prevNodes[dragIndex] as Item],
111
],
112
})
113
);
114
}, []);
115
116
const renderNode = useCallback((id: number, index: number, node: ReactNode) => {
117
return <Node key={id} index={index} id={id} node={node} moveNode={moveNode} />;
118
// eslint-disable-next-line react-hooks/exhaustive-deps
119
}, []);
120
121
return <>{itemList.map((item, index) => renderNode(item.id, index, item.node))}</>;
122
}
Copied!

Usage

1
const cards = useRef([
2
{
3
id: 1,
4
text: "Write a cool JS library",
5
},
6
{
7
id: 2,
8
text: "Make it generic enough",
9
},
10
{
11
id: 3,
12
text: "Write README",
13
},
14
{
15
id: 4,
16
text: "Create some examples",
17
},
18
{
19
id: 5,
20
text: "Spam in Twitter and IRC to promote it (note that this element is taller than the others)",
21
},
22
{
23
id: 6,
24
text: "The origin text here is a symbol that is not ok",
25
},
26
{
27
id: 7,
28
text: "PROFIT",
29
},
30
]);
31
32
...
33
34
<DndSortable
35
nodeList={cards.current.map((card, i) => {
36
return (
37
<div key={i} className="dnd-sortable">
38
{card.text}
39
</div>
40
);
41
})}
42
/>
Copied!

Example

Reference

  1. 1.
    The react-dnd official simple sortable example: Sortable Simple
  2. 2.
    React DnD