Ekekenta Odionyenfe I am a software engineer and technical writer who is proficient in server-side scripting and database setup.

Building a Trello Clone with React DnD

10 min read 2845

React DND Trello Clone

Building applications with drag-and-drop functionality can be overly complicated using traditional JavaScript. React DnD is a set of utilities that simplifies transferring data between different parts of your application, allowing you to easily create high-performance interfaces with drag-and-drop functionality.

React DnD is a perfect fit for apps like Trello, ProofHub, and ClickUp, which provide a UI for organizing your projects into different boards with drag and drop. In this tutorial, we’ll explore React DnD by building our own Trello clone. The code used in this tutorial is available on GitHub. Let’s get started!

React DnD’s unique features

React DnD includes a slew of great features that will save you development time and boost your application’s efficiency. Let’s review some of React DnD’s major perks.

In a technique similar to React Router and Flummox, React DnD covers your components, injecting props into them rather than offering prebuilt widgets. React DnD is built on React’s declarative rendering philosophy and therefore doesn’t modify the DOM. React DnD is an excellent complement to Redux and other unidirectional data flow architectures.

React DnD uses HTML5 drag and drop by default, but alternately, you can use the backend of your choice, creating your own custom events. Although drag and drop in HTML5 has a somewhat complicated API with several browser issues, React DnD handles these automatically, allowing you more time to focus on your project instead of troubleshooting.

Building a Trello clone with React DnD

Now that we understand the fundamentals behind React DnD, let’s build our Trello clone! First, create a new React project using the npx create-react-app command. For this tutorial, we’ll name our project trello-clone:

npx create-react-app trello-clone

Wait for the installation to finish, then change directories to the newly created folder. Next, we need to install React DnD and HTML drag and drop with the command below:

npm install react-dnd react-dnd-html5-backend react-modal

Now, let’s create our application’s components. In the src directory, create a component folder.

Create a Column component

First, we’ll create a Column component that will serve as a wrapper for the other components we’ll create later in this tutorial. In Column, we’ll display all the content for our cards. Create a Column.jsx file and add the JSX code snippet below:

import react from "react";
const Column = ({isOver,children})=>{
    const className = isOver ? "Highlight-region" : ""
    return (
      <div className={`col${className}`}></div>,
        {children}
    );
};
export default Column;

The code above creates a column where we’ll display all our cards. We’ll also need to apply a different style to our card when an item is dragged to it. We’ll use a ternary operator and a className variable, which will change depending on the state of the card.

Create a CardItem component

Let’s create another component called ItemCard to handle our actual card items. We’ll also create a CardItem.jsx file inside our src/component folder.

First, we’ll import the following:

  • useEffect Hook: performs side effects in your components
  • useState Hook: manages our application’s state
  • useRef Hook: persists objects throughout our component
  • Fragment: enables us to group a list of child nodes
  • Window: refers to the Window component, which we’ll create shortly
  • TEMS_TYPE: specifies the type of item in our card

Run the command below to install the items listed above:

import react, { userEffect, Fragment, useRef, useState } from "react";
import { useDrag, useDrop } from 'react-dnd';
import Window from './Window';
import ITEM_TYPES from '../data/types'

Note that we haven’t created the data folder or its files yet. We’ll do that in a later section.

Next, let’s create our CardItem component, which will be responsible for moving items within the cards. Then, we’ll create a useRef Hook and destructure the drop object from the React useDrop Hook provided by React DnD.

We only need the drop object, so we’ll pass a , as the parameter of our object. We accept our item types, then pass our item and monitor to the hover parameter:

const cardItem = ({ item, index, moveItem, status }) => {
    const ref = useRef(null)
    const [, drop ] = useDrop({
        accept: ITEM_TYPES,
        hover({ item, monitor }) {
        ......

We’ll also check whether the item that we dragged was dropped in another card. If the item was not moved to another card, we don’t have to do anything to return the item:

if (!ref.current) {
    return;
}
.....

However, if our item was dragged to another card, we need to move the item to that card:

const dragIndex = item.index;
const hoverIndex = item.hover;
if (dragIndex === hoverIndex) {
  return;
}
const hovererdRect = ref.current.getBoundClientRect();
const hoverMiddleY = hovererdRect.bottom - hovererdRect.top / 2;
const mousePostion = monitor.getClientOffest();
const hoverClientY = mousePostion.y - hovererdRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
  return;
}
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
  return;
}
moveItem(dragIndex, hoverIndex);
item.index = hoverIndex;
......

In the code above, we created a dragIndex variable to check the index of the selected item. We created the hoverIndex variable to indicate the index of the item that is being moved. When the itemIndex is the same as the hoverItem, it means the item was not moved, in which case we’ll do nothing.

Then, we find hoverdRect, the hovered item rectangle position, hoverMiddleY, the hovered item middle y-axis position, mousePostion, the hovered item mouse position, and lastly, overClientY, the hovered item client y-axis position.

We performed a few checks to ensure that the selected item is properly moved to the intended card. Then, we moved the card and updated the index of the moved item.



Next, we need to destructure isDragging and drag from the useDrag Hook, which will require an item. In this case, we’ll use our item types, our actual items, and the item index. isDragging also take a collect callback function, which gives us a lot of props data supplied by React DnD.

Among the data provided by the collect callback is monitor, which refers to a copy of the screen that lets us know if we are dragging the screen:

 const [{ isDragging }, drag] = useDrag({
        item: { type: ITEM_TYPES, ...item, index },
        collect: monitor => ({
            isDragging: monitor.isDragging()
        })
});
.......

We’ll create a state to handle the opening and closing of our card window, with onOpen and onClose handler functions to change the state of the card window being dragged. Then, we wrap the drag Hook with the drop Hook and pass in our ref Hook, helping us identify and locate the item that we’re working with:

const [show, setShow] = useState(false);
const onOpen = () => setShow(true);
const onClose = () => setShow(false)
drag(drop(ref))
.......

Finally, we’ll use the <Fragment> component to return our grouped JSX element, displaying the card item data. When the object is in dragging mode, we set different opacity values and background colors from when it is dropped on a card. Then, we have our Window component rendered with the show value, item, and onClose function handler passed to it:

   return (
        <Fragment >
            <div
                ref={ref}
                style={{ opacity: isDragging ? 0 : 1 }}
                className={'item'}
                onClick={onOpen}
            >
                <div className={'color-bar'} style={{ backgroundColor: status.color }}
                ></div>
                <p className={'item-title'}>{item.content}</p>
            </div>
            <Window 
               item = {item}
               onClose = {onclose}
               show = {show}
            />

        </Fragment>
    )
}
export default cardItem

Create a Window component

Now that we’re done with our CardItem component, let’s quickly create our Window component. In our src/component folder, create a Window.jsx file and add the following code snippet to it:

import React from "react";
import Modal from "react-modal";
Modal.setAppElement("#app");

In the code above, we import React and Modal from react-modal. We set the Modal to display on our application by using Modal.setAppElement and passing our app root element ID.

Next, we create our Window functional component, which gives us three props, show, onClose handler, and item. The show prop tells us whether to show the window or not. onClose controls what happens when the Window is closed, and Item props is the actual item in the Window:

&gt;const Window = ({ show, onClose, item }) => {
.......

Then, we create our Modal and pass in the required props, isOpen, onRequestClose, and overlayClassName. We show the details of the items on the Modal and add a button to handle the Modal closing:

 return (
    <Modal
      isOpen={show}
      onRequestClose={onClose}
      className={"modal"}
      overlayClassName={"overlay"}
    >
      <div className={"close-btn-ctn"}>
        <h1 style={{ flex: "1 90%" }}>{item.title}</h1>
        <button className={"close-btn"} onClick={onclose}>
          X
        </button>
      </div>
      <div>
        <h2>Description</h2>
        <p>{item.content}</p>
        <h2>Status</h2>
        <p>
          {item.icon}{" "}
          {`${item.status.charAt(0).toUpperCase()}${item.status.slice(1)}`}
        </p>
      </div>
    </Modal>
  );
};
export default Window;

Create the DropContainer component

We’re done with our Window component, but we need a parent container that will enclose our Column and CardItem components as its child components, enabling us to have an indication of our card movements. First, we need to create a DropContainer.jsx file in our src/component folder and add the code snippet below to it:

import React from "react";
import { useDrop } from "react-dnd";
import ITEM_TYPES from "../data/types";
import { statuses } from "../data";

The code above will import the items types, which are statuses we created in our data folder to serve as our database, storing dummy data for our application.

Next, we create our DropContainer functional component, which also provides us with three props, onDrop, children, and status. We’ll need to destructure isOver and drop from the useDrop Hook, which takes accept, canDrop, Drop, and collect callbacks.

Then, we pass in our ITEM_TYPES to the accept callback and item and monitor to the canDrop callback. To know whether we can actually drop the item on a given card with the canDrop callback, we’ll get our item index and status.

Now, we can return an array of our items and their indexes, enabling the movement of our items either forward or backward on our cards:

const DropContainer = ({ onDrop, children, status }) => {
  const [{ isOver }, drop] = useDrop({
    accept: ITEM_TYPES,
    canDrop: (item, monitor) => {
      const itemIndex = statuses.findeIndex((si) => si.status === item.status);
      const statusIndex = statuses.findIndex((si) => si.index === status);
      return [itemIndex + 1, itemIndex - 1, itemIndex].includes(statusIndex);
    },
........

On our onDrop callback, we’ll take an item and the monitor, which are the two parameters that we’ll need to drop an item. Then, we call the onDrop, passing the item, monitor, and the status of the item we are dropping.

We’ll use the collect callback to set the isOver props, which know if the item has been dropped in a card:

drop: (item, monitor) => {
      onDrop(item, monitor, status);
    },
    collect: (monitor) => ({
      isOver: monitor.isOver(),
    }),
  });
........

Finally, we’ll return our HTML, referencing the drop Hook. We will use the React.cloneElement method to pass in a bunch of children elements to our parent component. Then, we export our DropContainer component:

return (
    <div ref={drop} className={"drop-wrapper"}>
      {React.cloneElement(children, { isOver })}
    </div>
  );
};
export default DropContainer;

Create the Home component

Now let’s go ahead to create our homepage component. First, create a Home.jsx file in our src/component folder and add the code snippets below to it:

import React, { useState } from "react";
import CardItem from "./CardItems";
import DropContainer from "./DropConatiner";
import Column from "./Column";
import { data, statuses } from "../data";
...........

The code above imports our CardItem, DropContainer, and the Column component into our Home.jsx component. The code also imports data and statuses from our data folder.


More great articles from LogRocket:


Next, we create a state in our Home functional component to get an array of all the items in our data. We move the indexes of our items and change their status with the onDrop handler:

const Home = (props) => {
  const [items, setItems] = useState(data);
  const onDrop = (item, monitor, status) => {
    setItems(prevState => {
      const newItems = prevState
        .filter(i => i.id !== item.id)
        .concat({ ...item, status);
      return [...newItems];
    });
  .........

We can control what happens when the item is moving by creating a moveItem handler, which takes two arguments, dragIndexand hoverIndex.

dragIndex is for the item being dragged, while hoverIndex is for items on hover. We filter the items’ array to get the items whose index is currently on the dragIndex object and insert the item into the new card:

 const moveItem = (dragIndex, hoverIndex) => {
    const item = items[dragIndex];
    setItems(prevState => {
      const newItems = prevState.filter((i, idx) => idx !== dragIndex);
      newItems.splice(hoverIndex, 0, item);
      return [...newItems];
    });
  };
...........

Next, we’ll loop through all our statuses using the map function, and return our columns with their status names. We’ll pass in our DropContainer, which accepts the OnDrop Hook and status. Now, we’ll display our columns, which will display only the items with the status in each of the columns. Then, we export our Home component:

  return (
    <div className={"row"}>
      {statuses.map(s =&gt; {
        return (
          <div key={s.status} className={"col-wrapper"}>
            <h4 className={"col-header"}>{s.status.toUpperCase}</h4>
            <DropWrapper onDrop={onDrop} status={s.status}>
              <Col>
                {items
                  .filter(i => i.status === s.status)
                  .map((i, idx) => (
                    <Item
                      key={i.id}
                      item={i}
                      index={idx}
                      moveItem={moveItem}
                      status={s}
                    />
                  ))}
              </Col>
            </DropWrapper>
          </div>
        );
      })}
    </div>
  );

Now, update the /src/App.jsx file with the following JSX code snippet:

........
import Homepage from "./component/Homepage";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
const App = () => {
  return (
    <DndProvider backend={HTML5Backend}>
      <div className={"row"}>
        <p className={"page-header"}>Trello Clone Dashboard</p>
      </div>
      <Homepage />
    </DndProvider>
  );
};
........

The code above wraps our entire application with the React DndProvider, passing the HTML5Backend,  which we import from the React dnd HTML5 backend.

Next, make a data folder in our src directory folder. In the data folder, create a data.js and a types.js file. Add the code snippet below to the data.js file:

const data = [
  {
    id: 1,
    status: "open",
    title: "Available Topic",
    content: "Buiding a REST API with Django",
  },
  {
    id: 2,
    status: "open",
    title: "Sponsored Post",
    content: "How to create a React Chat Application",
  },
  {
    id: 3,
    status: "open",
    title: "Editing",
    content: "Building a Trello clone with React DnD",
  },
  {
    id: 4,
    status: "open",
    title: "Invoicing",
    content: "Inro To Web3 ",
  },
];
const statuses = [
  {
    index: 1,
    status: "open",
    color: "#EB5A46",
  },
  {
    index: 2,
    status: "in progress",
    color: "#00C2E0",
  },
  {
    index: 3,
    status: "in review",
    color: "#C377E0",
  },
  {
    index: 4,
    status: "done",
    color: "#3981DE",
  },
];
export { data, statuses };

Then, add the code snippet below to the types.js file:

const ITEM_TYPES = 'ITEM'
export default ITEM_TYPES

Styling our components

Now, let’s style our components. Create a style.css file in your src directory and add the code snippet below to it:

:root {
    --primary-color: rgb(62, 100, 255);
    --complete-color: #27aa80;
    --text-color: #172b4d;
    --disabled-color: #fad6d6;
    --background-color: #f5eaea;
}
html {
    background: rgb(0,73,191);
    background: linear-gradient(90deg, rgba(0,73,191,1) 0%, rgba(190,190,255,1) 46%, rgba(0,212,255,1) 100%);
}
body {
    color: var(--text-color);
    font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Droid Sans,Helvetica Neue,sans-serif;
    margin: 0;
    background-image: url("https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2400x1600/ef3a46a026c718b8329c0c34b0a57108/photo-1550592704-6c76defa9985.jpg");
    background-size: cover;
}
a {
    color: unset;
    text-decoration: unset;
    cursor: pointer;
}
p {
    margin: 10px 0;
    overflow-wrap: break-word;
    text-align: left;
}
label {
    font-size: 16px;
    display: block;
}
button, input {
    padding: 4px;
    border: 1px solid var(--disabled-color);
}
button {
    outline: none;
    background: transparent;
    border-radius: 5px;
    color: var(--primary-color);
    transition: all ease 0.8s;
    cursor: pointer;
}
button.active {
    color: var(--primary-color);
}
button.active:after {
    content: "";
    display: block;
    margin: 0 auto;
    width: 50%;
    padding-top: 4px;
    border-bottom: 1px solid var(--primary-color);
}
input:focus {
    outline: none;
}
select {
    outline: none;
    height: 40px;
}
.row {
    display: flex;
    flex-direction: row;
    justify-content: center;
}
.item {
    font-size: 15px;
    margin-bottom: 10px;
    padding: 10px;
    border-radius: 5px;
    z-index: 1;
    background-color: white;
}
.item:hover {
    cursor: pointer;
}
.item-title {
    font-weight: 600;
    font-size: 16px;
}
.item-status {
    text-align: right;
}
.color-bar {
    width: 40px;
    height: 10px;
    border-radius: 5px;
}
.drop-wrapper {
    flex: 1 25%;
    width: 100%;
    height: 100%;
}
.col-wrapper {
    display: flex;
    flex-direction: column;
    margin: 20px;
    padding: 20px;
    background-color: var(--background-color);
    border-radius: 5px;
}
.col-header {
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 20px;
    margin-top: 0;
}
.col {
    min-height: 300px;
    max-width: 300px;
    width: 300px;
}
.highlight-region {
    background-color: yellow;
}
.page-header {
    background-color: #10131470;
    padding: 20px;
    color: white;
    font-size: 20px;
    flex: 1 100%;
    margin-top: 0;
    text-align: left;
    font-weight: bolder;
}
.modal {
    background-color: #F4F5F7;
    border-radius: 2px;
    margin: 48px 0 80px;
    min-height: 450px;
    width: 800px;
    outline: none;
    padding: 20px;
}
.overlay {
    display: flex;
    justify-content: center;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0,0,0,0.5);;
}
.close-btn-ctn {
    display: flex;
}
.close-btn {
    height: 40px;
    width: 35px;
    font-size: 20px;
    color: #031D2C;
    border: none;
    border-radius: 25px;
}
.close-btn:hover {
    background-color: #DCDCDC;
}

Conclusion

In this tutorial, we learned how we can easily implement drag-and-drop functionality in our applications using React DnD. To put our knowledge into practice, we built a clone of Trello, a UI tool for organizing projects into columns using drag and drop.

You can follow the steps outlined in this tutorial to build all kinds of applications that require drag and drop. I hope you enjoyed this tutorial!

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ekekenta Odionyenfe I am a software engineer and technical writer who is proficient in server-side scripting and database setup.

Leave a Reply