Creating a drag and drop Kanban board in React

Amit PRAmit PR
6 min read

Being able to move the elements through drag and drop is a cool feature we see in many web-based applications like to-do lists, file drag and drop and many more. In this post I'd be discussing a simple drag and drop implementation in React using React beautiful DnD - a popular React library used to achieve drag and drop feature in your applications.

First, we would create a simple React application using the create-react-app package and install react beautiful dnd package. I'd also be using Tailwind CSS in this but that would be completely optional. Create a folder called 'dnd-client', open this folder in the terminal.

create-react-app .
npm install react-router-dom
npm install react-beautiful-dnd

We would need some sample data to drag and drop. Let's create a folder named 'data', inside this folder create a JS file called 'items.js'. Put the following code in this file

const initialData = {

    columns: {
        'Sunday': {
            id: 'Sunday',
            title: 'To do',
            taskIds: ['task-1', 'task-2', 'task-3', 'task-4', 'Source'],
        },
        'Monday': {
            id: 'Monday',
            title: 'In progress',
            taskIds: ['task-5', 'task-6'],
        },
        'Tuesday': {
            id: 'Tuesday',
            title: 'Active',
            taskIds: ['task-7', 'task-8'],
        },
        'Wednesday': {
            id: 'Wednesday',
            title: 'More',
            taskIds: [],
        },
        'Thursday': {
            id: 'Thursday',
            title: 'Bark More',
            taskIds: [],
        },
        'Friday': {
            id: 'Friday',
            title: 'Good Friday',
            taskIds: [],
        },
        'Saturday': {
            id: 'Saturday',
            title: 'Party',
            taskIds: [],
        }
    },
    // facilitate the ordering of columns
    columnOrder: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
};

export default initialData;

Go to the 'App.js' file and initialize routing for this app, just some housekeeping stuff before we get into the good part.

import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import ExampleScreen from "./pages/ExampleOne";

const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<ExampleScreen />} exact />
      </Routes>
    </Router>
  );
};

export default App;

Now create a folder 'pages', inside this folder we would have our example file called 'ExampleOne' which we have imported in the above code within the router.

import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import initialData from "../data/items";

const ExampleOne = () => {
  function handleOnDragEnd(result) {
    // This gets triggered once a drop and drop event happens
  }

  return (
    <div className="App">
      <DragDropContext onDragEnd={handleOnDragEnd}>
          <div className="dropped-content">
            {initialData.columnOrder.map((name, index) => {
              return (
                <Droppable key={name} droppableId={name} index={index}>
                  {(provided) => (
                    <div
                      className="dropped-container"
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                    >
                      <h3>{name}</h3>

                      {initialData.columns[name].taskIds.map((item, index) => {
                        return (
                          <Draggable
                            key={item}
                            draggableId={item}
                            index={index}
                          >
                            {(provided) => (
                              <p
                                className="drop-list-item list-none text-red-400"
                                ref={provided.innerRef}
                                {...provided.draggableProps}
                                {...provided.dragHandleProps}
                              >
                                {item}
                              </p>
                            )}
                          </Draggable>
                        );
                      })}
                      {provided.placeholder}
                    </div>
                  )}
                </Droppable>
              );
            })}
          </div>
        </DragDropContext>
    </div>
  );
};

export default ExampleOne;

Ok, so now we have imported core components from the react beautiful dnd library which are DragDropContext, Droppable and Draggable. We wrap our root div where we want to catch all the drag and drop events inside this 'DragDropContext' wrapper. There are two types of components that we should be able to be dragged and there should be entities that should listen to the dropped events occurring inside the context. If we look at the data structure of the "initialData" variable, we have an object which contains all seven days as keys having some nested task data for each of them. To order them, we maintain another list with all these days.

We want each day to be able to respond to the drop events for the tasks, it is a simple kanban board application where we should be able to drag and drop tasks under any given day of the week. The change should persist after the event happens which we'd address later using useState and handleOnDragEnd. We have wrapped the day list inside the droppable wrapper provided by the library.

<Droppable key={name} droppableId={name} index={index}>
  {(provided) => (
     <div
       className="dropped-container"
       ref={provided.innerRef}
       {...provided.draggableProps}
       {...provided.dragHandleProps}
     >
...

Each droppable entity has a unique id called droppableId to identify it uniquely. it provides some configuration options under provided parameter which can be used to tweak the Drag and drop events but we shall refrain from discussing it in detail here for the sake of simplicity. This also creates a reference binding it to the HTML DOM and capturing drag-and-drop events on it.

We would place the tasks for each day as a list inside the days of the week. We want each of these items to be draggable. Like droppable, we also have a wrapper to apply drag events on an item with props and provider. Draggable also expects each element wrapped inside it to have a distinct identifier. For styling, I've applied some Tailwind classes which you can ignore for now.

<Draggable
   key={item}
   draggableId={item}
   index={index}
   >
   {(provided) => (
   <p
       className="drop-list-item list-none text-red-400"
       ref={provided.innerRef}
       {...provided.draggableProps}
       {...provided.dragHandleProps}
       >
       {item}
   </p>
   )}
</Draggable>

The library expects each droppable component to have a placeholder at the end so we have added {provided.placeholder}. Till now, if everything is done correctly we should be able to move around the task blocks across the days column. But, we won't be able to save the updated results for which we'd make some changes in the event handler function called 'handleOnDragEnd'.

This function was left empty in the above code, but now we'd populate it. We would also have the feature of generating additional task blocks and be able to move them as well aside from previously existing blocks in the list imported.

const [stateData, updateStateData] = useState(initialData);
const [counter, setCounter] = useState(9);

function handleOnDragEnd(result) {
    if (!result.destination) return;

    // remove from source array and put in destination array
    let newStateData = { ...stateData };
    let destinationArray = Array.from(
      stateData.columns[result.destination.droppableId].taskIds
    );
    let sourceArray = Array.from(
      stateData.columns[result.source.droppableId].taskIds
    );

    if (result.draggableId !== 'Source') {
      const itemInserted = sourceArray[result.source.index];
      sourceArray.splice(result.source.index, 1);
      destinationArray.splice(result.destination.index, 0, itemInserted);
      newStateData.columns[result.source.droppableId].taskIds = sourceArray;
      newStateData.columns[result.destination.droppableId].taskIds = destinationArray;
    } else {
      let itemCreated = 'item-' + counter.toString();
      setCounter(counter+1)
      destinationArray.push(itemCreated)
      newStateData.columns[result.destination.droppableId].taskIds = destinationArray;
    }
    updateStateData(newStateData);
    // console.log("New state data ", newStateData, result);
  }

We have used two variables to keep track of the updated data and the count of the task blocks. A functional component is used for the demonstration hence we are using useState hook to update the data. Inside the handler, we have a 'result' parameter that contains draggableId, droppableId, source, destination and index related to the drag and drop event that happened. We can utilize these options to update the state and update the DOM. We create two arrays called sourceArray and destinationArray from source and destination days which we can extract from the 'result' variable. If we are dragging the 'source' element, then a new block would be generated and gets appended to the destination day column where the event took place.

If any normal task block is dragged and dropped where destination and source droppable Ids are different then, it would be clipped from the source array and appended into the destination array. Javascript array functions like splice and push.

Then, we update the current state of the data appending the updated array under the selected days where drag and dropped happened. This way the change would be persistent. Here's the linked repo with this min-project

https://github.com/Apfirebolt/kanban-board-in-react-using-react-beautiful-dnd

That is all for now, folks. You should have a very basic drag-and-drop layout in React with the ability to generate new blocks. For any doubts and queries please fill free to post in the comments section.

0
Subscribe to my newsletter

Read articles from Amit PR directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amit PR
Amit PR