← Go back to blog
Awa Dieudonne Mbuh
Jan 1315 min read

Build a Multi-List Drag and Drop To-Do App Using React-beautiful-dnd

In this article, we will be building a Multi-List Drag and Drop To-Do App using React-beautiful-dnd in Next.js/Chakra UI.
Build a Multi-List Drag and Drop To-Do App Using React-beautiful-dnd

Before we begin, make sure you have the following prerequisites:

  1. A Next.js application. If you don't have one, you can create a new Next.js app using the following command:

npx create-next-app my-app

  1. Chakra UI configured on the Next.js application. You can follow this link to set it up: Next.js Chakra installation guide

  2. Configure a Chakra UI theme for the application. You can do that by creating a theme.js file in the src folder and pasting the following code:

// src/theme.ts
import { extendTheme, theme } from "@chakra-ui/react";

const colors = {
  "main-bg": "#0E1012",

  "white-text": "#E8E8EA",
  "subtle-text": "#9B9B9B",

  "column-bg": "#16181D",
  "column-header-bg": "#1A1D23",

  "card-bg": "#242731",
  "card-border": "#2D313E"
};

const fonts = {
  heading: "Poppins",
  body: "Poppins",
};

export default extendTheme({
  ...theme,
  colors,
  fonts,
});

Make sure you update _app.js accordingly.

Having addressed the prerequisites, let's move on to designing the page in the pages/index.js file. But before that, you need to install react-beautiful-dnd by running the command: yarn add react-beautiful-dnd

Setting up the context

The first step in using react-beautiful-dnd is to wrap the elements that will handle the drag and drop in a DragDropContext component. This component provides the context that the Draggable and Droppable components will use to interact with each other. Here's an example of how to set up the DragDropContext:

// pages/index.js
import { DragDropContext } from 'react-beautiful-dnd';

function Home() {
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      {/* Your columns go here */}
    </DragDropContext>
  );
}

You need to pass an onDragEnd callback function that will be called when a drag-and-drop event is completed. In this callback function, you can update the state of your application with the new order of the items.

function onDragEnd(result) {
  // Update the state of your application with the new order of the items
  // after a drag-and-drop event
}

Creating a droppable area

The next step is to create a droppable area where items can be dropped. This is done using the Droppable component. The Droppable component takes a droppableId prop that is used to identify the droppable area.

Inside our pages/index.js, we are going to update the component by adding code for the droppable area:

// pages/index.js
import { useState } from 'react';
import { Flex, Heading, Text } from "@chakra-ui/react";
import { Droppable } from 'react-beautiful-dnd';

function Home() {
const [state, setState] = useState(initialData);

function onDragEnd(result) {
  // Update the state of your application with the new order of the items
  // after a drag-and-drop event
}

 return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Flex
        flexDir="column"
        bg="main-bg"
        minH="100vh"
        w="full"
        color="white-text"
        pb="2rem"
      >
        <Flex py="4rem" flexDir="column" align="center">
          <Heading fontSize="3xl" fontWeight={600}>
            React Beautiful Drag and Drop
          </Heading>
          <Text fontSize="20px" fontWeight={600} color="subtle-text">
            react-beautiful-dnd
          </Text>
        </Flex>

        <Flex justify="space-between" px="4rem">
          {state.columnOrder.map((columnId) => {
            const column = state.columns[columnId];
            const tasks = column.taskIds.map((taskId) => state.tasks[taskId]);

            return <Droppable droppableId={column.id}>
                {(droppableProvided, droppableSnapshot) => (
                     {/* Your droppable code here */}
                )}
            </Droppable >
          })}
        </Flex>
      </Flex>
    </DragDropContext>
  );
}

const initialData = {
  tasks: {},
  columns: {
    "column-1": {},
    "column-2": {},
    "column-3": {},
  },
  // Facilitate reordering of the columns
  columnOrder: ["column-1", "column-2", "column-3"],
};

Creating draggable items

The final step is to make the items in the list draggable. This is done using the Draggable component. The Draggable component takes a draggableId prop that is used to identify the item and an index prop that is used to specify the item's position in the list.

To do this, we are going to create a Column component to render the TODO, IN-PROGRESS, and the COMPLETED columns respectively.

// src/components/Column.js
import { Flex, Text } from "@chakra-ui/react";
import React from "react";
import { Draggable, Droppable } from "react-beautiful-dnd";

const Column = ({ column, tasks }) => {
  return (
    <Flex rounded="3px" bg="column-bg" w="400px" h="620px" flexDir="column">
      <Flex
        align="center"
        h="60px"
        bg="column-header-bg"
        rounded="3px 3px 0 0"
        px="1.5rem"
        mb="1.5rem"
      >
        <Text fontSize="17px" fontWeight={600} color="subtle-text">
          {column.title}
        </Text>
      </Flex>

      <Droppable droppableId={column.id}>
        {(droppableProvided, droppableSnapshot) => (
          <Flex
            px="1.5rem"
            flex={1}
            flexDir="column"
            ref={droppableProvided.innerRef}
            {...droppableProvided.droppableProps}
          >
            {tasks.map((task, index) => (
              <Draggable key={task.id} draggableId={`${task.id}`} index={index}>
                {(draggableProvided, draggableSnapshot) => (
                  <Flex
                    mb="1rem"
                    h="72px"
                    bg="card-bg"
                    rounded="3px"
                    p="1.5rem"
                    outline="2px solid"
                    outlineColor={
                      draggableSnapshot.isDragging
                        ? "card-border"
                        : "transparent"
                    }
                    boxShadow={
                      draggableSnapshot.isDragging
                        ? "0 5px 10px rgba(0, 0, 0, 0.6)"
                        : "unset"
                    }
                    ref={draggableProvided.innerRef}
                    {...draggableProvided.draggableProps}
                    {...draggableProvided.dragHandleProps}
                  >
                    <Text>{task.content}</Text>
                  </Flex>
                )}
              </Draggable>
            ))}
          </Flex>
        )}
      </Droppable>
    </Flex>
  );
};

export default Column;

In the above code, we're using the map function to loop through the tasks and creating a Draggable component for each task. We're also using the key and index props to identify the item and its position in the list.

To wrap up, at the end your pages/index.js should have the following code:

// pages/index.js
import { Flex, Heading, Text } from "@chakra-ui/react";
import dynamic from "next/dynamic";
import React, { useState } from "react";
import { DragDropContext } from "react-beautiful-dnd";

const Column = dynamic(() => import("../src/Column"), { ssr: false });

const reorderColumnList = (sourceCol, startIndex, endIndex) => {
  const newTaskIds = Array.from(sourceCol.taskIds);
  const [removed] = newTaskIds.splice(startIndex, 1);
  newTaskIds.splice(endIndex, 0, removed);

  const newColumn = {
    ...sourceCol,
    taskIds: newTaskIds,
  };

  return newColumn;
};

export default function Home() {
  const [state, setState] = useState(initialData);

  const onDragEnd = (result) => {
    const { destination, source } = result;

    // If user tries to drop in an unknown destination
    if (!destination) return;

    // if the user drags and drops back in the same position
    if (
      destination.droppableId === source.droppableId &&
      destination.index === source.index
    ) {
      return;
    }

    // If the user drops within the same column but in a different positoin
    const sourceCol = state.columns[source.droppableId];
    const destinationCol = state.columns[destination.droppableId];

    if (sourceCol.id === destinationCol.id) {
      const newColumn = reorderColumnList(
        sourceCol,
        source.index,
        destination.index
      );

      const newState = {
        ...state,
        columns: {
          ...state.columns,
          [newColumn.id]: newColumn,
        },
      };
      setState(newState);
      return;
    }

    // If the user moves from one column to another
    const startTaskIds = Array.from(sourceCol.taskIds);
    const [removed] = startTaskIds.splice(source.index, 1);
    const newStartCol = {
      ...sourceCol,
      taskIds: startTaskIds,
    };

    const endTaskIds = Array.from(destinationCol.taskIds);
    endTaskIds.splice(destination.index, 0, removed);
    const newEndCol = {
      ...destinationCol,
      taskIds: endTaskIds,
    };

    const newState = {
      ...state,
      columns: {
        ...state.columns,
        [newStartCol.id]: newStartCol,
        [newEndCol.id]: newEndCol,
      },
    };

    setState(newState);
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Flex
        flexDir="column"
        bg="main-bg"
        minH="100vh"
        w="full"
        color="white-text"
        pb="2rem"
      >
        <Flex py="4rem" flexDir="column" align="center">
          <Heading fontSize="3xl" fontWeight={600}>
            React Beautiful Drag and Drop
          </Heading>
          <Text fontSize="20px" fontWeight={600} color="subtle-text">
            react-beautiful-dnd
          </Text>
        </Flex>

        <Flex justify="space-between" px="4rem">
          {state.columnOrder.map((columnId) => {
            const column = state.columns[columnId];
            const tasks = column.taskIds.map((taskId) => state.tasks[taskId]);

            return <Column key={column.id} column={column} tasks={tasks} />;
          })}
        </Flex>
      </Flex>
    </DragDropContext>
  );
}

const initialData = {
  tasks: {
    1: { id: 1, content: "Configure Next.js application" },
    2: { id: 2, content: "Configure Next.js and tailwind " },
    3: { id: 3, content: "Create sidebar navigation menu" },
    4: { id: 4, content: "Create page footer" },
    5: { id: 5, content: "Create page navigation menu" },
    6: { id: 6, content: "Create page layout" },
  },
  columns: {
    "column-1": {
      id: "column-1",
      title: "TO-DO",
      taskIds: [1, 2, 3, 4, 5, 6],
    },
    "column-2": {
      id: "column-2",
      title: "IN-PROGRESS",
      taskIds: [],
    },
    "column-3": {
      id: "column-3",
      title: "COMPLETED",
      taskIds: [],
    },
  },
  // Facilitate reordering of the columns
  columnOrder: ["column-1", "column-2", "column-3"],
};

You can now run the command below to test the app.

yarn dev

You should have something similar to the screenshot below. Multi-list drag and drop

Conclusion

react-beautiful-dnd is a powerful and easy-to-use library for implementing drag-and-drop functionality in a React application. By following the steps outlined in this article, you can quickly add drag-and-drop functionality to a list of items in your application.

If you found this tutorial helpful and want to stay up-to-date with the latest web development trends and techniques, be sure to subscribe to our newsletter or our channel at FullStack Matery. We will notify you of new articles, tech updates, and early access to exclusive content. Don't miss out on the opportunity to grow as a developer and stay ahead of the curve!

Lastly, let me know what you think about the article by leaving a comment below.

Thanks!

JavaScript
Next.js
React.js
Chakra UI
Styled-components
Comments(0)