Exploring Smooth Drag-and-Drop with React Beautiful DnD

Miko Dagatan
Miko Dagatan August 22, 2024

Introduction

For front-end developers, it's important to ensure that the user interface provides a seamless experience for the user. Front-end developers may have encountered needing to implement the drag-and-drop functionality to their projects. This can be a daunting functionality to implement when starting from scratch. Luckily for us developers, there's react-beautiful-dnd.

 

What is React Beautiful DnD?

React Beautiful DnD is a React library developed by Atlassian, the creators of Trello and JIRA. It is designed to implement smooth and accessible drag-and-drop interactions in React applications.

 

Getting Started:

To begin using React Beautiful DnD in your React project, you can install it via npm or yarn:

 


 npm install react-beautiful-dnd

or


 yarn add react-beautiful-dnd

 

Example:

react-beautiful-dnd_example

 

react-beautiful-example

 

In my application, I have a ProjectLists which contains List child components, and each List company has many Card child components.

 

Let's focus first on the DragDropContext . This is the all-encompassing component that wraps over all react-beautiful-dnd components. It passes data on the onDragEnd prop, and that's where you create your logic on moving all the draggables to the droppables.

 

Next is the Droppable component. We have this first as we want to drag and drop lists and arrange them. It has the props droppableId="project-list", type="column", and direction="horizontal". Supplying the droppableId and direction is important as it allows the package to know which component the draggable items will be arranged at. For type it's also important to determine the difference of drag and dropping columns (list) or cards.

 

Under the Droppable component, we have the {(provided, snapshot) => (children)}, and under this should have a single descendant. In my case it's < div > and we supply the ref={provided.innerRef} and {...provided.droppableProps} there. Under that we have the container of the lists, < ProjectListsContainer > , then we map the lists. It's good to note that below the lists, you add the {provided.placeholder} so that the animation / movement of the cards are smooth.

 


ProjectLists.jsx

...imports...
import { DragDropContext, Droppable } from "react-beautiful-dnd";


export const ProjectLists = () => {
  const [project, setProject] = useRecoilState(projectState);
  const setCard = useSetRecoilState(cardState);

  const onDragEnd = async (result: any) => {
    ...
  };

  const sendMoveCard = async (result: any) => {
    const res = await moveCard({
      project_id: project?.id,
      card_id: extractId(result.draggableId),
      source_column_id: extractId(result.source.droppableId),
      source_index: result.source.index,
      destination_column_id: extractId(result.destination.droppableId),
      destination_index: result.destination.index,
    });
    return res;
  };

  const sendMoveList = async (result: any) => {
    await moveList({
      column_id: extractId(result.draggableId),
      destination_index: result.destination.index,
    });
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      
        {(provided, snapshot) => (
          <div ref={provided.innerRef} {...provided.droppableProps}>
            
              {project.columns &&
                project.columns.map((list: ListType, index: number) => (
                  <List list={list} key={index} index={index} />
                ))}
              {provided.placeholder}
              
            
          </div>
        )}
      
    </DragDropContext>
  );
};

 

The code below shows the < Draggable > and the < Droppable > components in the List component. For < Draggable > , this is the code that makes the list drag and droppable to the < ProjectLists > . You need to supply a draggableId which needs a unique id. You also need to supply the index and the key. Under the Draggable Component, the first child should have a {(provided, snapshot) = > (children)} wherein you supply {...provided.draggableProps} and ref={provided.innerRef} to the closest single descendant. In my case, I'm using the < div > for that and it wraps all items under it.

 

Within the < Draggable > component, you supply the < Droppable > component. This is where we show that the Within the < List > should have cards dragging and dropping within it. It should have a droppableId, direction, and type. The type is card so that react-beautiful-dnd knows the difference of moving columns/lists and cards. Immediately under the , we have a {(provided, snapshot) = > (children)} wherein you supply {...provided.draggableProps}, {...provided.dragHandleProps} and ref={provided.innerRef} to the closest single descendant. In my case, the < div > makes it easy to wrap the < Card > component within. You should supply the {provided.placeholder} as the last child of the div. </p>

 

Under this, we have the < Draggable > component which wraps the Cards.

 


List.jsx
...imports...

import { Draggable, Droppable } from "react-beautiful-dnd";

export const List = ({ list, index }: ListProps) => {
  ...variables...

  ...methods...

  return (
    <Draggable
      draggableId={`draggableList-${list?.id}`}
      index={index}
      key={`${list?.id}`}
    >
      {(provided, snapshot) => (
        <div ref={provided.innerRef} {...provided.draggableProps}>
          
            <ListHeader
              {...provided.dragHandleProps}
              $hasNoCards={!!!list.cards.length}
            >
              {isEdit ? (
                <>
                  
<HeaderInput onChange={onChangeName} value={list.name} ref={nameRef} onKeyDown={(e) => onEnter(e, onClickCloseName)} />
<CloseNameButton onClick={onClickCloseName}> </CloseNameButton> <DeleteColumnButton onClick={onClickDeleteColumn}> </DeleteColumnButton> </> ) : ( <Header onDoubleClick={onDoubleClickName}>{list.name}</Header> )} <AddCardButton onClick={onClickAddCard}> </AddCardButton> </ListHeader> <Droppable droppableId={`droppableList-${list.id}`} direction="vertical" type="card" > {(provided, snapshot) => ( <div ref={provided.innerRef} {...provided.droppableProps}> {list.cards?.map((card: CardType, index) => ( <Draggable draggableId={`draggableCard-${card?.id}`} index={index} key={`${card?.id}`} > {(provided, snapshot) => ( <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} > <Card card={card} /> </div> )} </Draggable> ))} {provided.placeholder} </div> )} </Droppable>
</div> )} </Draggable> ); }; ); }; export default App;

 

Dragging and Dropping Logic

When you've put all the components in place, one of the most important part you need to consider is what the react-beautiful-dnd is supplying to the onDragEnd. So you're taking the data and you'll write the logic behind every movement in the app. In my case, there's the horizontal movement of the < List > and the vertical movement of the < Card > .

 

These are the important values of the result, which came from the onDragEnd prop.

 


  result.source.index
  result.source.droppableId
  result.destination.index
  result.destination.droppableId
  result.type

 

In the code below, it shows that there are different actions if the result.type is column and if the result.type is card . Using RecoilJS, I've set up a projectState that has all the Lists/Columns and the cards inside. So that's the reason why my onDragEnd handler looks like that. For some reason, in order to get the results I wanted, I also needed to clone the columns. I've worked hard on the logic, but I hope sharing this will help you out in adopting react-beautiful-dnd .


export const clone = (obj: any) => {
    return JSON.parse(JSON.stringify(obj));
  };

 


  ProjectLists.jsx

  export const ProjectLists = () => {
  const [project, setProject] = useRecoilState(projectState);
  const setCard = useSetRecoilState(cardState);

  const onDragEnd = async (result: any) => {
    if (!!!result.destination) {
      return;
    }

    const newLists = clone(project.columns);
    if (result.type == "column") {
      const [removed] = newLists.splice(result.source.index, 1);
      newLists.splice(result.destination.index, 0, removed);
      setProject({ ...project, columns: newLists });
      await sendMoveList(result);
    } else if (result.type == "card") {
      const columnSource = newLists.find(
        (column: ListType) =>
          column.id.toString() == extractId(result.source.droppableId)
      );
      const [removed] = columnSource!.cards!.splice(result.source.index, 1);
      const columnTarget = newLists.find(
        (column: ListType) =>
          column.id.toString() == extractId(result.destination.droppableId)
      );
      const newCard = {
        ...removed,
        columnId: extractId(result.destination.droppableId),
      };
      columnTarget!.cards!.splice(result.destination.index, 0, newCard);
      setProject({ ...project, columns: newLists });

      const res = await sendMoveCard(result);
      setCard(res.card);
    }
  };

  const sendMoveCard = async (result: any) => {
    const res = await moveCard({
      project_id: project?.id,
      card_id: extractId(result.draggableId),
      source_column_id: extractId(result.source.droppableId),
      source_index: result.source.index,
      destination_column_id: extractId(result.destination.droppableId),
      destination_index: result.destination.index,
    });
    return res;
  };

  const sendMoveList = async (result: any) => {
    await moveList({
      column_id: extractId(result.draggableId),
      destination_index: result.destination.index,
    });
  };

  return (
    ...shown above...
  );
};

 

Known Bugs and Fixes

I have come across some bugs wherein the draggable element is positioned far away from the cursor when dragging. You may probably have come across this too, but there's a solution for this.

In the code below, style is added to the draggable element with left and top properties. Try it on your code if you encounter this problem!

 


  <Draggable
    draggableId={`${task.type}-${task.id}`}
    index={index}
    key={task.id}
  >
    {(provided, snapshot) => (
      <div
        ref={provided.innerRef}
        {...provided.draggableProps}
        {...provided.dragHandleProps}
        style=
      >
        <Task task={task} disabled={disabled} />
      </div>
    )}
  </Draggable>

 

Conclusion

react-beautiful-dnd is a powerful tool that simplifies drag-and-drop interactions in React applications. It is the perfect tool to use when dealing with a simple to-do list or a complex kanban board. It gives out performance, accessibility, and flexibility, so it has become a popular choice among React developers. I have also used this on my applications and it gives out a smooth experience on my user interactions. Give it a try on your next project, it will surely elevate your drag-and-drop experience!

 

Ps. if you have any questions

Ask here