Dragging and dropping items is a common user interaction pattern that can greatly enhance the user experience in web applications. When building sortable lists with drag handles in React.js, the Dnd-kit library provides a powerful and flexible solution.
Getting Started
In this post, you will see how to accomplish this in a simple react component and also in a complex form that uses react hook form. For the complex part, I will build the sortable list in an already existing invoice generator app.
Installing Dnd-kit library
Dnd-kit is a modern drag-and-drop library for React, making it easy to implement complex drag-and-drop interfaces. Before we dive into building our sortable list, let's make sure we have Dnd-kit installed in our project:
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
Implementation
Simple Implementation
Let's assume we have 2 components: Items (array of products) and SingleItem. In the items component you map over the products and put the single item inside like this:
const Items = () => {
const [products, setProducts] = useState(/* Some products here */)
return (
{
products.map((product, index) => (
<SingleItem
key={product.id}
product={product}
/>
))
}
)
}
const SingleItem = ({product}) => {
return (
<div>
{/* Display item here */}
</div>
)
}
To set up the initial sortable list, wrap the products.map with these:
// Dnd-kit imports
import {
DndContext,
closestCenter,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
DragEndEvent
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
const Items = () => {
const [products, setProducts] = useState(/* Some products here */)
// Sensors
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragEnd = useCallback(
async (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(active.id);
if (active.id !== over?.id) {
const oldIndex = products.findIndex(
(item) => item.id === active.id
);
const newIndex = products.findIndex(
(item) => item.id === over?.id
);
// Rearrange the items
const updatedProducts = arrayMove(products, oldIndex, newIndex);
setProducts(updatedProducts)
}
},
[products, setValue]
);
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={products}
strategy={verticalListSortingStrategy}
>
{
products.map((product, index) => (
<SingleItem
key={product.id}
product={product}
/>
))
}
</SortableContext>
</DndContext>
)
}
Next, we will add useSortable hook from dnd-kit to SingleItem component
const SingleItem = ({product}) => {
// DnD
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: product.id });
const style = {
transition,
transform: CSS.Transform.toString(transform),
};
return (
<div
style={style}
{...attributes}
>
{/* Display item here */}
{/* Drag and Drop grip button. */}
<div ref={setNodeRef} {...listeners}>
{/* This is an icon from Lucide icons */}
<GripVertical />
</div>
</div>
)
}
The parameter id
inside useSortable has to be a unique identifier for this product
Complex Implementation inside an invoice generation app using react-hook-form
Again, let's assume we have 2 components: Items and SingleItem.
Unlike the simple implementation, let's say we get the products from a useFieldArray
method from react-hook-form like this:
const { products, append, remove, move } = useFieldArray({
control: control,
name: ITEMS_NAME,
});
This method is useful when you have an array of things inside a form that you need to keep track of.
The append and remove functions are optional, but in this case, they are used to add or remove certain items from the invoice:
const addNewField = () => {
append({
name: "",
description: "",
quantity: 0,
unitPrice: 0,
total: 0,
});
};
const removeField = (index: number) => {
remove(index);
};
Our old handleDragEnd
logic for dnd context, now will not work properly, so let's change up a few things.
Everything else can stay the same as with simple implementation, except we will change one little detail in the handleDragEnd
function.
We will use the move
method from useFieldArray
and replace it with old rearrange logic:
Replace this:
// Rearrange the items
const updatedProducts = arrayMove(products, oldIndex, newIndex);
setProducts(updatedProducts)
With this:
// New rearrange logic for react-hook-form implementation
move(oldIndex, newIndex);
Now, the products can be grabbed and reordered properly.
Optional additions
Optionally, you can also add a few methods that can move a product up or down one time when clicking on certain buttons.
const moveFieldUp = (index: number) => {
if (index > 0) {
move(index, index - 1);
}
};
const moveFieldDown = (index: number) => {
if (index < fields.length - 1) {
move(index, index + 1);
}
};
Now, you need to pass the index and these functions to the SingleItem
component.
Next, you accept these new properties in SingleItem
and place them accordingly:
const SingleItem = ({product, index, moveFieldUp, moveFieldDown}) => {
return (
<div>
{/* Display item and grip handler here */}
<button onClick={() =>moveFieldUp(index)}>
Move up
</button>
<button onClick={() => moveFieldDown(index)}>
Move down
</button>
</div>
)
}
The final implementation looks like this:
Loading...