July 7, 2021
Adding drag-and-drop functionality with react-beautiful-dnd

If you’ve ever worked with Jira, Trello, Confluence, or any other Atlassian product, you’ve likely encountered a drag-and-drop functionality that enables users to drag items through multiple (and sometimes huge) lists. It’s an incredibly useful feature that always seems to work smoothly, but building this functionality into an app can be challenging.

What is react-beautiful-dnd?

Enter react-beautiful-dnd, Atlassian’s open source library that allows web developers to easily integrate drag-and-drop functionality into their applications. react-beautiful-dnd is currently the most popular drag-and-drop library in the React world, and it’s the way to go if you want to implement a drag-and-drop feature on your site.

First, let’s look at some of the important features of react-beautiful-dnd and see what makes it so popular.

It is highly customizable. The library tries to be as headless as possible, which means there are only some basic stylings around the animation. They are adjustable and can be replaced by your own implementation. You can even implement your own sensors to control the drag-and-drop lists with additional input sources other than a mouse, such as a keyboard (there are no limits, as shown in the examples)
No additional DOM nodes are created, which means that building the layout is simple and predictable, ensuring you can easily design your drag-and-drop items and lists
Along with the first point, react-beautiful-dnd provides a wide range of options and metadata that allow you to create endless combinations and different use cases with it

Check out a comprehensive list of all the features from the official documentation.

Using react-beautiful-dnd components for drag-and-drop functionality

The library is deeply integrated into the React ecosystem, meaning that all functionality is built and controlled around key React components.

DragDropContext

This is the context that contains all the information about your drag-and-drop lists. It is based on the React context, so you should wrap everything related to drag-and-drop with this element to make it work properly. Additionally, it accepts the onDrag function, which is the key callback for controlling the data of your lists. We will go into more detail later.

Droppable

This element contains your list and the drop zone that will be the source when elements are dropped. It must be identified with a droppableId, and it must be wrapped around a function that returns a React element. This function is called with some internal parameters and a snapshot parameter containing the information about the state of the list. This can be used for styling or callbacks for events around this element.

Draggable

This is a container for all list elements. You must wrap every single one of your list items with this element. Similar to the Droppable element, the Children element is a function that is called with the Snapshot property.

To demonstrate how these elements are used, I’ll show you some examples. We will create two basic examples to demonstrate the main features of the library. The first one will be simple and will show how the elements work together. The second will be more advanced and contain multiple lists, similar to a Trello board, to demonstrate the capabilities of this library.

Building a drag-and-drop list with react-beautiful-dnd

Data structure

To start, we need some data for the demonstration:

interface DataElement {
id: string;
content: string;
}

type List = DataElement[]

Within this structure, we want to visualize a list of DataElements and make it possible to drag and drop the elements within the list.

Setting up the drag-and-drop elements

Let’s first create the elements we need to get our list ready for dragging and dropping. As we learned above, the first step is to create a DragDropContext that encloses everything we want on our list.

In it, we create a droppable container and map over our DataElements, then render a Draggable element for each one. What is rendered for each element is up to you. To keep it simple, we’ll render a prepared ListElement component that internally solely takes the style of the elements to make it pretty, but is not relevant to the article.

function DragAndDropList() {
return (
<DragDropContext>
<Droppable droppableId=”droppable” >
{(provided, snapshot) => (
<div
{…provided.droppableProps}
ref={provided.innerRef}
>
{elements.map((item, index) => (
<Draggable draggableId={item.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{…provided.draggableProps}
{…provided.dragHandleProps}
>
<ListItem item={item} />
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
)
}

For the purpose of the example, we will also omit styling the list, but this would essentially display your data in a list with one below the other. The amazing part of using react-beautiful-dnd is that just by writing this code, it is already possible to grab elements. The list and elements also animate correctly.

However, as you can see in the following example, if you move the element to a different location, the element will briefly end up in the correct location, but the list will jump back to its previous state and the element will be in its original position.

The reason for this is that the order of the elements in the underlying data array remains the same, as react-beautiful-dnd does not manipulate your data and should not be responsible for doing so. Instead, react-beautiful-dnd will only animate the states and give you the proper callbacks to handle your data.

It’s our job to put the data in the right form at the right time. So, now we need to set a hook when the item is dropped and correctly manipulate our data.

First, let’s put our data in a React state to make it easier to change and manipulate:

function DragAndDropList() {
const [items, setItems] = useState(baseData)
return (…)
}

To hook into the lifecycle of the drag-and-drop animation, we can add an onDragEnd function to our DragDropContext:

<DragDropContext onDragEnd={onDragEnd}>

Now we need to implement this function inside our component to shape our state in the right way when this function is called. To get all the information we need, the function receives the following result object as an argument. This object has the following structure and properties:

interface DraggableLocation {
droppableId: string;
index: number;
}

interface Combine {
draggableId: string;
droppableId: string;
}

interface DragResult {
reason: ‘DROP’ | ‘CANCEL’;
destination?: DraggableLocation;
source: DraggableLocation;
combine?: Combine;
mode: ‘FLUID’ | ‘SNAP’;
draggableId: DraggableId;
}

This resulting object contains all of the information about how we need to manipulate our data so that it does not snap back to the previous position.

The two properties, destination and source, are the most important here. As the name suggests, source contains the information where the element was before and destination contains where the element was dropped.

Both properties contain the droppableId and index, both of which we need to manipulate our data array. Now that we have everything we need, let’s write down the logic of how we want to change our data array.

function DragAndDropList() {
const [items, setItems] = useState(baseData)

function onDragEnd(result) {
const newItems = […items];
const [removed] = newItems.splice(result.source.index, 1);
newItems.splice(result.destination.index, 0, removed);
setItems(newItems)
}

return (…)
}

Here, we used the source.index and the destination.index to change our data in the correct places.

Generally, this is all we need to have a fully functional drag-and-drop list. But before we continue, let’s add a small functionality to make it less error-prone. As you can see in the definition, the destination property can be undefined. This would mean that the element was not dropped into a droppable element.

Simply add the lines of code to prevent this kind of behavior:

function onDragEnd(result) {
if (!result.destination) {
return;
}
const newItems = […items];
const [removed] = newItems.splice(result.source.index, 1);
newItems.splice(result.destination.index, 0, removed);
setItems(newItems)
}

And that’s it! Here’s the whole flow in action.

Building multiple drag-and-drop lists with react-beautiful-dnd

Now that we’ve seen the basic functions, let’s look at a slightly more advanced — but more frequently used — use case.

Perhaps you have multiple lists and want to drag and drop items not only within each list but also between lists, as you would on a Kanban board. With react-beautiful-dnd, you can accomplish this. Let’s first render multiple lists and make it possible to drag and drop items from one list to another.

Similar to the first example, we need to wrap everything in DragDropContext. Additionally, we need to create a constant that contains the information about what lists we want and iterate over it to render all the desired lists elements:

const lists = [‘todo’, ‘inProgress’, “done”];

function DragList() {
return (
<DragDropContext>
<ListGrid>
{lists.map((listKey) => (
<List elements={elements[listKey]} key={listKey} prefix={listKey} />
))}
</ListGrid>
</DragDropContext>
);
}

The list element we return for each element in the array is almost the same as in the example above, with the only exception being that it does not contain the state of the data or the functionality to manipulate it.

So, we’ll only use the rendered part of the list, and it will look like this:

const DraggableElement = ({ prefix, elements }) => (
<Droppable droppableId={`${prefix}`}>
{(provided) => (
<div
{…provided.droppableProps}
ref={provided.innerRef}
>
{elements.map((item, index) => (
<ListItem key={item.id} item={item} index={index}/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
)

It’s critical to give each list and Droppable element a unique property ID, called droppableId. With this ID, we can later identify which list the element came from and which list it was dropped into.

Managing state

The tricky part is deciding where and how we want to keep our state. Because we can have items that change from list to list, we need to move the state up into the wrapping component that holds all of the lists. Let’s start by creating a state in the DragList component, which will look something like this:

const data = {
todo: [
{
id: ‘91583f67-0617-4df2-bd74-3c018460da6c’
listId: ‘todo’,
content: ‘random content’
},

]:
inProgress: […]
done: […]
}

Ideally, we would probably use a reducer function for the state, but to keep the example short, we’ll use useState hook. Notice that we have one large object that contains the data for all of our lists. As we did in our single drag-and-drop list example, we also create the function onDrag:

const removeFromList = (list, index) => {
const result = Array.from(list);
const [removed] = result.splice(index, 1);
return [removed, result]
}

const addToList = (list, index, element) => {
const result = Array.from(list);
result.splice(index, 0, element);
return result
}

const onDragEnd = (result) => {
if (!result.destination) {
return;
}
const listCopy = { …elements }

const sourceList = listCopy[result.source.droppableId]
const [removedElement, newSourceList] = removeFromList(sourceList, result.source.index)
listCopy[result.source.droppableId] = newSourceList

const destinationList = listCopy[result.destination.droppableId]
listCopy[result.destination.droppableId] = addToList(destinationList, result.destination.index, removedElement)

setElements(listCopy)
}

As you can see, the code itself looks quite similar to the single list example, but we now have to consider the respective list identified with the destination and the source property.

Fortunately for us, the result object always provides this information with the draggableId property, which is now quite useful for knowing from which lists the elements come from and where they are going next. You can now also drag and drop items between lists as well.

Here is the full example.

Conclusion

Drag-and-drop is a widely used functionality that can make your application powerful. The react-beautiful-dnd library provides all the functionality needed to incorporate drag-and-drop into your app. It’s flexible, unopinionated, and the animation works brilliantly.

As with many things in open-source programming, most problems can be accurately solved for (and by!) developers using libraries. It’s nice to see companies like Atlassian make their solution available to the rest of the world so that web developers don’t have to continuously build features from scratch.

The post Adding drag-and-drop functionality with react-beautiful-dnd appeared first on LogRocket Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send