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.
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.
Check out a comprehensive list of all the features from the official documentation.
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.
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.
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.
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.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.