← Back toMaster of React ref

Drag and drop items within a list

Written byPhuoc Nguyen
Created
15 Oct, 2023
Last updated
02 Dec, 2023
Tags
HTML5 drag drop, sortable list, useRef() hook
In our previous post, we learned about using the `useRef()` hook to create a reference of an element using the `ref` attribute. We also learned how to retrieve the instance of the element by accessing the `current` property of the ref.
But what if we want to manage the reference of an element that might change based on user interactions? In this post, we'll dive into how to set the value of the ref dynamically. We'll do this by building a sortable list component – a useful technique to have in your React toolkit.

What is a sortable list?

A sortable list is a user interface component that lets users drag and drop items within a list, allowing them to easily reorder the items in real-time. This type of interaction is useful in a variety of scenarios, including:
  • Reordering tasks in a to-do list
  • Sorting products by price, name or popularity
  • Rearranging items in a playlist
  • Changing the order of steps in a wizard
By providing an intuitive way for users to manipulate the order of items, sortable lists can greatly improve the user experience and increase engagement with your application. In the next section, we'll see how to implement this functionality using React and `useRef()`.

Simplifying drag-and-drop with HTML5

Implementing drag-and-drop functionality on a webpage can be tricky, but HTML5 makes it easy with a built-in feature. This post will show you how.
HTML5 introduced new attributes and events that enable native drag-and-drop functionality on web applications. The `draggable` attribute makes any element draggable, while the `ondragstart` event is fired when the dragging starts.
Additionally, the `ondragover` event is fired on the target element as long as the mouse pointer is within its boundaries during a drag operation. This allows us to detect potential drop targets and provide visual feedback to the user, such as highlighting or changing the cursor shape.
By using these features, we can create a smooth and responsive sorting experience that feels natural to users. In the next section, we'll dive into how we can leverage these capabilities in our sortable list component.

Building a sortable list component

Let's talk about building a sortable list component. For simplicity's sake, let's assume that our list will contain items with two properties: an `id`, which is unique to each item, and a `content` property, which represents the content of the item.
As we'll be updating the items with drag-and-drop actions, we'll use an internal state to manage them.
tsx
const [items, setItems] = React.useState([
{ id: 1, content: 'A' },
{ id: 2, content: 'B' },
{ id: 3, content: 'C' },
{ id: 4, content: 'D' },
{ id: 5, content: 'E' },
]);
We'll use the HTML5 attribute and events we introduced earlier to display the list of items. Here's an example of what the render function of the `SortableList` component could look like:
tsx
<div className="list">
{
items.map((item, index) => (
<div
key={item.id}
draggable='true'
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
>
{item.content}
</div>
))
}
</div>
In this example, we're looping over a list of items and using the map function to render each one. To help React identify which elements in the list have changed, been added, or been removed, we're using the `key` attribute. This attribute should be a unique identifier for each item in the list, so we're assigning it to `item.id`.
By doing this, we're helping React optimize the rendering process by only updating the items that have actually changed, instead of re-rendering the entire list every time. This can improve performance, especially when removing or inserting items into the middle of a list.
To make the items draggable, we're using the `draggable` attribute. We're also using the `onDragStart` and `onDragOver` events to track when we start dragging an item and when we drag it over another item in the list. To handle these events, we're using an additional state called `draggingIndex` to keep track of which item is being dragged. We're also using the `useRef()` hook to track the corresponding item.
ts
const [draggingIndex, setDraggingIndex] = React.useState(-1);
const dragNode = React.useRef();
When an item in a list is dragged, the `handleDragStart` function is called. It sets the `draggingIndex` state to the index of the dragged item and stores a reference to the corresponding DOM node in the `dragNode` ref.
To specify what types of operations are allowed on the data being dragged, we use the `e.dataTransfer.effectAllowed` property. In this case, we set it to `'move'` to allow reordering within the same list.
Finally, we use `e.dataTransfer.setData()` to set the data that will be transferred during the drag-and-drop operation. We pass two arguments: a MIME type (in this case, `'text/html'`) and the target element itself.
Here's an example code snippet to demonstrate what the `handleDragStart` function could look like:
ts
const handleDragStart = (e, index) => {
const { target } = e;
setDraggingIndex(index);

dragNode.current = target;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', target);
};
Let's dive into handling the `onDragOver` event. When a user drags an item over another item in the list, the `handleDragOver` function is called. We use `e.preventDefault()` to prevent the browser's default behavior, which would disallow dropping anything onto this element.
If we're not dragging over the same item that's being dragged (`dragNode.current !== e.target`), we create a copy of the items array using the spread operator and call `splice()` on it. This removes the dragged item from its current position (`newItems.splice(draggingIndex, 1)`) and inserts it at its new position (`newItems.splice(index, 0, ...)`).
By setting the state of both `items` and `draggingIndex`, React will re-render our list with the updated order of items. This way, we can see the changes in real-time as we drag and drop items within our sortable list component.
Here's an example of how to handle the `onDragOver` event:
ts
const handleDragOver = (e, index) => {
e.preventDefault();
if (dragNode.current !== e.target) {
let newItems = [...items];
newItems.splice(index, 0, newItems.splice(draggingIndex, 1)[0]);
setDraggingIndex(index);
setItems(newItems);
}
};
Let's check out our `SortableList` component in action!

Enhancing the flexibility of the sortable list component

In the previous section, we used a hard-coded list of items for the `SortableList` component.
To make the `SortableList` component more flexible and reusable, we can modify it to accept a `children` prop instead of using hard-coded items. This allows for greater customization and gives the user more control over each list item's content.
Here's an updated version of the `SortableList` component with a `children` prop:
tsx
const SortableList = ({ children }) => {
const clonedItems = React.useMemo(() => {
return React.Children.map(children, (child, index) => ({
id: index,
content: child,
}));
}, [children]);

const [items, setItems] = React.useState(clonedItems);

// ...

return (
<div>
{items.map((item) => (
<div
key={item.id}
draggable='true'
onDragStart={(e) => handleDragStart(e, item.id)}
onDragOver={(e) => handleDragOver(e, item.id)}
>
{item.content}
</div>
))}
</div>
);
};
In this updated version, we've introduced a new `SortableListProps` type that specifies the `children` prop as a `React.ReactNode`. Instead of hard-coding the content of each list item, we're now using `React.Children.map()` to iterate over the children and create an array of items with unique ids based on their index in the list.
This approach allows us to render any type of content inside each list item, such as images, links, or custom components. To improve performance, we're using the `useMemo` hook to memorize the cloned items array.
The `useMemo` hook caches the result of a function call and returns that value until one of its dependencies changes. By passing an array containing `[children]` as a second argument to `React.useMemo()`, we're telling React to only recompute the value of `clonedItems` if the `children` prop has changed since the last render.
Overall, this approach gives users more flexibility to customize the appearance and behavior of our sortable list component.

Demo

It's time to check out the final demo below.
If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn