DEV Community

Cover image for Drag and drop items within a list
Phuoc Nguyen
Phuoc Nguyen

Posted on • Edited on • Originally published at phuoc.ng

Drag and drop items within a list

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.

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' },
]);
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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.

const [draggingIndex, setDraggingIndex] = React.useState(-1);
const dragNode = React.useRef();
Enter fullscreen mode Exit fullscreen mode

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:

const handleDragStart = (e, index) => {
    const { target } = e;
    setDraggingIndex(index);

    dragNode.current = target;
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/html', target);
};
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
};
Enter fullscreen mode Exit fullscreen mode

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:

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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.

It's time to check out the final demo below.


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series 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!

If you want more helpful content like this, feel free to follow me:

Top comments (0)