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.
1<CodeSandbox title="rearrange gallery images" id="keen-zhukovsky-w4z2pq"/>
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:
1npx create-react-app <project-name>
After the installation, navigate to the created directory and run the command below to start the application:
1cd <project-name> # navigate to the directory23npm run start # run the development server
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:
1yarn add react-dnd react-dnd-html5-backend23# or45npm install react-dnd react-dnd-html5-backend
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.
1// src/index.js2// other imports34// add this5import { DndProvider } from "react-dnd";6import { HTML5Backend } from "react-dnd-html5-backend";7import App from "./App";8import "./styles.css";910const rootElement = document.getElementById("root");11const root = createRoot(rootElement);1213root.render(14 <StrictMode>15 <DndProvider backend={HTML5Backend}>16 <App />17 </DndProvider>18 </StrictMode>19);
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.
Project: Gallery Images
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.
1# src/data.js2export default [3 {4 id: 1,5 title: 'Travel',6 img:7 '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='8 },9...
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:
1// src/App.js2import React from "react";3import galleryList from "./data.js";45const Card = ({src, title, id, index}) => {6 return (7 <div className="card">8 <img src={src} alt={title} />9 </div>10 );11};12const App = () => {13 const [images, setImages] = React.useState(galleryList);1415return (16 <main>17 {React.Children.toArray(18 images.map((image, index) => (19 <Card20 src={image.img}21 title={image.title}22 id={image.id}23 index={index}24 />25 ))26 )}27 </main>28);29};30export default App;
The result from the above should look like this:
Implementing Drag and Drop on Images with React DnD
With the library present in our app, we import the useDrag
hook from React DnD.
1// src/App.js2// react import3import { useDrag} from "react-dnd";4// image data objects import56const Card = ({src, title, id, index}) => {7const ref = React.useRef(null);8 return (9 <div className="card">10 // card images11 </div>12 );13};1415const App = () => {16 // state17const [{ isDragging }, drag] = useDrag({18 type: "image",19 item: () => {20 return { id, index };21 },22 collect: (monitor) => {23 return {24 isDragging: monitor.isDragging()25 };26 }27});2829return (30 <main>31 // loop through the images in Card component32 </main>33);34};35export default App;
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.
Defining a Drop Area
Let’s import the useDrop
component from the React DnD library.
1// src/App.js2import { useDrag, useDrop } from "react-dnd";
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:
1// React import2import { useDrag, useDrop } from "react-dnd";34// data image import56const Card = ({ src, title, id, index }) => {7const ref = React.useRef(null);89const [, drop] = useDrop({10 accept: "image",11 hover: (item, monitor) => {12 if (!ref.current) {13 return;14 }15const dragIndex = item.index;16const hoverIndex = index;1718if (dragIndex === hoverIndex) {19 return;20}21const hoverBoundingRect = ref.current?.getBoundingClientRect();22const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;2324const clientOffset = monitor.getClientOffset();25const hoverClientY = clientOffset.y - hoverBoundingRect.top;2627if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {28 return;29}3031if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {32 return;33}3435moveImage(dragIndex, hoverIndex);3637item.index = hoverIndex;3839}40});4142// useDrag component4344const opacity = isDragging ? 0 : 1;45drag(drop(ref));4647return (48 <div ref={ref} style={{ opacity }} className="card">49 <img src={src} alt={title} />50 </div>51);52}5354// App component
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.
Rearrange the Gallery Images with React DnD
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.
1// import23// Card component45const App = () => {6 // state78// add this9const moveImage = React.useCallback((dragIndex, hoverIndex) => {10 setImages((prevCards) => {11 const clonedCards = [...prevCards];12 const removedItem = clonedCards.splice(dragIndex, 1)[0];13 clonedCards.splice(hoverIndex, 0, removedItem);14 return clonedCards;15});16}, []);1718return (19 <main>20 {React.Children.toArray(21 images.map((image, index) => (22 <Card23 src={image.img}24 title={image.title}25 id={image.id}26 index={index}27 moveImage={moveImage} // add this28 />29 ))30 )}31 </main>32);33};34export default App;
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.
1import React from "react";2import { useDrag, useDrop } from "react-dnd";3import galleryList from "./data.js";4const Card = ({ src, title, id, index, moveImage }) => {5 const ref = React.useRef(null);6 const [, drop] = useDrop({7 accept: "image",8 hover: (item, monitor) => {9 if (!ref.current) {10 return;11 }12 const dragIndex = item.index;13 const hoverIndex = index;14 if (dragIndex === hoverIndex) {15 return;16 }17 const hoverBoundingRect = ref.current?.getBoundingClientRect();18 const hoverMiddleY =19 (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;20 const clientOffset = monitor.getClientOffset();21 const hoverClientY = clientOffset.y - hoverBoundingRect.top;22 if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {23 return;24 }25 if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {26 return;27 }28 moveImage(dragIndex, hoverIndex);29 item.index = hoverIndex;30 }31 });32 const [{ isDragging }, drag] = useDrag({33 type: "image",34 item: () => {35 return { id, index };36 },37 collect: (monitor) => {38 return {39 isDragging: monitor.isDragging()40 };41 }42 });43 const opacity = isDragging ? 0 : 1;44 drag(drop(ref));45 return (46 <div ref={ref} style={{ opacity }} className="card">47 <img src={src} alt={title} />48 </div>49 );50};51const App = () => {52 const [images, setImages] = React.useState(galleryList);53 const moveImage = React.useCallback((dragIndex, hoverIndex) => {54 setImages((prevCards) => {55 const clonedCards = [...prevCards];56 const removedItem = clonedCards.splice(dragIndex, 1)[0];57 clonedCards.splice(hoverIndex, 0, removedItem);58 return clonedCards;59 });60 }, []);61 return (62 <main>63 {React.Children.toArray(64 images.map((image, index) => (65 <Card66 src={image.img}67 title={image.title}68 id={image.id}69 index={index}70 moveImage={moveImage}71 />72 ))73 )}74 </main>75 );76};77export default App;
Conclusion
This post discussed how to rearrange gallery images with the drag and drop feature of React DnD.