An Easy to Use React DnD Sortable Component
I hope this is actually easy to use...
Last updated
I hope this is actually easy to use...
Last updated
Easy to use
Less code
You can simply copy the following code to a dnd-sortable.tsx
file, and use as a component.
import React, { FC, ReactNode, useCallback, useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import type { XYCoord, Identifier } from "dnd-core";
import update from "immutability-helper";
export interface NodeProps {
id: any;
index: number;
node: ReactNode;
moveNode: (dragIndex: number, hoverIndex: number) => void;
}
interface DragItem {
index: number;
id: string;
type: string;
}
export const Node: FC<NodeProps> = ({ id, index, node, moveNode: moveNode }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
accept: "item",
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item: DragItem, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items' height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
moveNode(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: "item",
item: () => {
return { id, index };
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0.5 : 1;
drag(drop(ref));
return (
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
{node}
</div>
);
};
export interface Item {
id: number;
node: ReactNode;
}
export default function DndSortable({ nodeList }: { nodeList: Array<ReactNode> }) {
// Set id for every react node for react-dnd to use
const [itemList, setItemList] = useState(nodeList.map((node, index) => ({ id: index, node: node })));
const moveNode = useCallback((dragIndex: number, hoverIndex: number) => {
setItemList((prevNodes: Item[]) =>
update(prevNodes, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, prevNodes[dragIndex] as Item],
],
})
);
}, []);
const renderNode = useCallback((id: number, index: number, node: ReactNode) => {
return <Node key={id} index={index} id={id} node={node} moveNode={moveNode} />;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <>{itemList.map((item, index) => renderNode(item.id, index, item.node))}</>;
}
const cards = useRef([
{
id: 1,
text: "Write a cool JS library",
},
{
id: 2,
text: "Make it generic enough",
},
{
id: 3,
text: "Write README",
},
{
id: 4,
text: "Create some examples",
},
{
id: 5,
text: "Spam in Twitter and IRC to promote it (note that this element is taller than the others)",
},
{
id: 6,
text: "The origin text here is a symbol that is not ok",
},
{
id: 7,
text: "PROFIT",
},
]);
...
<DndSortable
nodeList={cards.current.map((card, i) => {
return (
<div key={i} className="dnd-sortable">
{card.text}
</div>
);
})}
/>
The react-dnd official simple sortable example: Sortable Simple