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 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.
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.
Column
componentFirst, 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.
CardItem
componentLet’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 componentsuseState
Hook: manages our application’s stateuseRef
Hook: persists objects throughout our componentFragment
: enables us to group a list of child nodesWindow
: refers to the Window
component, which we’ll create shortlyTEMS_TYPE
: specifies the type of item in our cardRun 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
Window
componentNow 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
:
>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;
DropContainer
componentWe’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;
Home
componentNow 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.
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, dragIndex
and 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 => { 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
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; }
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!
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 nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.