React drag and drop (React DnD) is a beautiful and accessible drag and drop library made for React apps that let’s build interactive experiences. It supports touch interactions on mobile devices and HTML5, but it depends on touch support. One popular app that uses this drag and drop interaction is Trello.
In this post, we will learn how to build a page filled with images that allow anyone to move an image around to another position within the app in response to the drag and drop events.
The complete project is on CodeSandbox.
## Prerequisites
To complete the exercise in this post, we need the following:
- Basic knowledge of JavaScript and React.
- Node.js installed on our computer.
## Getting Started
React is a JavaScript frontend library for generating and building user interfaces for web applications.
To scaffold a new project, run the following command in our terminal:
```sh
npx create-react-app <project-name>
After the installation, navigate to the created directory and run the command below to start the application:
cd <project-name> # navigate to the directory
npm run start # run the development server
Code language: PHP (php)
React.js starts the development environment on http://localhost:3000
.
With the whole setup done, run either of the commands below to install the react-dnd
****and ****react-dnd-html5-backend
libraries to our project:
yarn add react-dnd react-dnd-html5-backend
# or
npm install react-dnd react-dnd-html5-backend
Code language: PHP (php)
react-dnd-html5-backend
: This library will allow the use of the drag and drop API with react``-``dnd
.
Now, let’s implement the drag and drop functionality in the project’s entry point , index.js
, with the provider component.
// src/index.js
// other imports
// add this
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import App from "./App";
import "./styles.css";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
</StrictMode>
);
Code language: JavaScript (javascript)
In the code block above, we wrap the highest order component, App
, with a provider that comes with the React DnD library. The provider serves to alert the library that the component inside the provider will have access to the library’s functionality. Also, we pass in the backend
property, which is the primary backend support for the touch support.
To have access to the image on the page, we will start with creating a list of images in our app that will have the ability to drag and drop the desired image.
We won’t be diving deep into building the gallery image app from scratch for this post. However, we will focus our attention more on the functionality of using the React DnD library to rearrange the image.
Now, create an array of objects that contains the data.
# src/data.js
export default [
{
id: 1,
title: 'Travel',
img:
'https://media.istockphoto.com/photos/beautiful-view-of-amazing-sandstone-formations-in-famous-lower-near-picture-id807387518?k=20&m=807387518&s=612x612&w=0&h=G8O867Id71Sk6LCiAnAYp-dIu9uf4YqlfnO3uj-SQ00='
},
...
Code language: PHP (php)
As pointed out earlier, we won’t be focusing much on the styling of the app. Create a new file in the src
folder called styles.css
and paste the following code from this gist.
Next, let’s loop through the resulting data objects in the App.js
file to see the gallery images from the Card
component by passing props. Add the following code:
// src/App.js
import React from "react";
import galleryList from "./data.js";
const Card = ({src, title, id, index}) => {
return (
<div className="card">
<img src={src} alt={title} />
</div>
);
};
const App = () => {
const [images, setImages] = React.useState(galleryList);
return (
<main>
{React.Children.toArray(
images.map((image, index) => (
<Card
src={image.img}
title={image.title}
id={image.id}
index={index}
/>
))
)}
</main>
);
};
export default App;
Code language: JavaScript (javascript)
The result from the above should look like this:
With the library present in our app, we import the useDrag
hook from React DnD.
// src/App.js
// react import
import { useDrag} from "react-dnd";
// image data objects import
const Card = ({src, title, id, index}) => {
const ref = React.useRef(null);
return (
<div className="card">
// card images
</div>
);
};
const App = () => {
// state
const [{ isDragging }, drag] = useDrag({
type: "image",
item: () => {
return { id, index };
},
collect: (monitor) => {
return {
isDragging: monitor.isDragging()
};
}
});
return (
<main>
// loop through the images in Card component
</main>
);
};
export default App;
Code language: JavaScript (javascript)
In the above code block, it does the following:
-
useRef
: This hook returns a mutableref
object when initialized to the passed argument with a.current
property. -
useDrag
: The hook is used on every element to make it draggable. Using theuseDrag
hook comes with a set of properties. -
isDragging
: A boolean variable that determines if we are currently dragging an image or not. It returnstrue
if we are dragging the image and false if otherwise. -
drag
: Used to reference the element we want to make draggable. -
type
: Every element in React DnD requires an identifier which is a string. -
item
: It describes the item (id
andindex
) property that match the drop targets about the drag source. -
collect
: The property contains a callback function that receives theisDragging
props andmonitor
parameter. -
monitor
: It allows you update the props of your components in response to the drag and drop state changes.
Let’s import the useDrop
component from the React DnD library.
// src/App.js
import { useDrag, useDrop } from "react-dnd";
Code language: JavaScript (javascript)
useDrop
: This hook acts as a drop target in our component into the DnD system.
Next, update and add the following code in the App.js
file:
// React import
import { useDrag, useDrop } from "react-dnd";
// data image import
const Card = ({ src, title, id, index }) => {
const ref = React.useRef(null);
const [, drop] = useDrop({
accept: "image",
hover: (item, monitor) => {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
moveImage(dragIndex, hoverIndex);
item.index = hoverIndex;
}
});
// useDrag component
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (
<div ref={ref} style={{ opacity }} className="card">
<img src={src} alt={title} />
</div>
);
}
// App component
Code language: JavaScript (javascript)
The above code does the following:
- drop: It is called when a compatible item on the app is dropped on the target.
- hover(item, monitor): The hover function helps to move things around.
-
!ref.current: Check whether the current attribute doesn’t exist at the defined variable ref from the
useRef
hook. We stop the function by passing the return keyword. - Next is to check whether the
dragIndex
andhoverIndex
are equal, meaning the item moved or dragged should do nothing if the item is not moved up and down the list of items with the return keyword stopping the function from firing. - hoverBoundingRect: It determines the rectangle on screen on the current attribute.
- hoverMiddleY: Get the vertical middle.
- clientOffset: This variable determines the mouse position.
- hoverClientY: It returns the pixels to the top.
- Next, we check the
dragIndex
,hoverIndex
,hoverClientY
, andhoverMiddleY
are equal; otherwise, stop the funtion.
For the tutorial to be complete, we need to be able to move the image from one location to
another. Add the following code to the App
component.
// import
// Card component
const App = () => {
// state
// add this
const moveImage = React.useCallback((dragIndex, hoverIndex) => {
setImages((prevCards) => {
const clonedCards = [...prevCards];
const removedItem = clonedCards.splice(dragIndex, 1)[0];
clonedCards.splice(hoverIndex, 0, removedItem);
return clonedCards;
});
}, []);
return (
<main>
{React.Children.toArray(
images.map((image, index) => (
<Card
src={image.img}
title={image.title}
id={image.id}
index={index}
moveImage={moveImage} // add this
/>
))
)}
</main>
);
};
export default App;
Code language: JavaScript (javascript)
The code above does the following:
- We updated the
Card
component with themoveImage
props and called it in its component. - Using the useCallback hook, we rerender the updated cards by updating them with
setImages
- The
splice
method on themoveImage
variable replaces the previous image and adds the new dragged image in its position.
Below is the complete code for the App.js
file.
import React from "react";
import { useDrag, useDrop } from "react-dnd";
import galleryList from "./data.js";
const Card = ({ src, title, id, index, moveImage }) => {
const ref = React.useRef(null);
const [, drop] = useDrop({
accept: "image",
hover: (item, monitor) => {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
moveImage(dragIndex, hoverIndex);
item.index = hoverIndex;
}
});
const [{ isDragging }, drag] = useDrag({
type: "image",
item: () => {
return { id, index };
},
collect: (monitor) => {
return {
isDragging: monitor.isDragging()
};
}
});
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (
<div ref={ref} style={{ opacity }} className="card">
<img src={src} alt={title} />
</div>
);
};
const App = () => {
const [images, setImages] = React.useState(galleryList);
const moveImage = React.useCallback((dragIndex, hoverIndex) => {
setImages((prevCards) => {
const clonedCards = [...prevCards];
const removedItem = clonedCards.splice(dragIndex, 1)[0];
clonedCards.splice(hoverIndex, 0, removedItem);
return clonedCards;
});
}, []);
return (
<main>
{React.Children.toArray(
images.map((image, index) => (
<Card
src={image.img}
title={image.title}
id={image.id}
index={index}
moveImage={moveImage}
/>
))
)}
</main>
);
};
export default App;
Code language: JavaScript (javascript)
This post discussed how to rearrange gallery images with the drag and drop feature of React DnD.