How to Rearrange Gallery Images Using React DnD

Banner for a MediaJam post

Christian Nwamba

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 directory
2
3npm 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-backend
2
3# or
4
5npm 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.js
2// other imports
3
4// add this
5import { DndProvider } from "react-dnd";
6import { HTML5Backend } from "react-dnd-html5-backend";
7import App from "./App";
8import "./styles.css";
9
10const rootElement = document.getElementById("root");
11const root = createRoot(rootElement);
12
13root.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.js
2export 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.js
2import React from "react";
3import galleryList from "./data.js";
4
5const 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);
14
15return (
16 <main>
17 {React.Children.toArray(
18 images.map((image, index) => (
19 <Card
20 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.js
2// react import
3import { useDrag} from "react-dnd";
4// image data objects import
5
6const Card = ({src, title, id, index}) => {
7const ref = React.useRef(null);
8 return (
9 <div className="card">
10 // card images
11 </div>
12 );
13};
14
15const App = () => {
16 // state
17const [{ 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});
28
29return (
30 <main>
31 // loop through the images in Card component
32 </main>
33);
34};
35export default App;

In the above code block, it does the following:

  • useRef: This hook returns a mutable ref object when initialized to the passed argument with a .current property.
  • useDrag: The hook is used on every element to make it draggable. Using the useDrag hook comes with a set of properties.
  • isDragging: A boolean variable that determines if we are currently dragging an image or not. It returns true 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 and index) property that match the drop targets about the drag source.
  • collect: The property contains a callback function that receives the isDragging props and monitor 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.js
2import { 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 import
2import { useDrag, useDrop } from "react-dnd";
3
4// data image import
5
6const Card = ({ src, title, id, index }) => {
7const ref = React.useRef(null);
8
9const [, drop] = useDrop({
10 accept: "image",
11 hover: (item, monitor) => {
12 if (!ref.current) {
13 return;
14 }
15const dragIndex = item.index;
16const hoverIndex = index;
17
18if (dragIndex === hoverIndex) {
19 return;
20}
21const hoverBoundingRect = ref.current?.getBoundingClientRect();
22const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
23
24const clientOffset = monitor.getClientOffset();
25const hoverClientY = clientOffset.y - hoverBoundingRect.top;
26
27if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
28 return;
29}
30
31if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
32 return;
33}
34
35moveImage(dragIndex, hoverIndex);
36
37item.index = hoverIndex;
38
39}
40});
41
42// useDrag component
43
44const opacity = isDragging ? 0 : 1;
45drag(drop(ref));
46
47return (
48 <div ref={ref} style={{ opacity }} className="card">
49 <img src={src} alt={title} />
50 </div>
51);
52}
53
54// 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 and hoverIndex 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, and hoverMiddleY 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// import
2
3// Card component
4
5const App = () => {
6 // state
7
8// add this
9const 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}, []);
17
18return (
19 <main>
20 {React.Children.toArray(
21 images.map((image, index) => (
22 <Card
23 src={image.img}
24 title={image.title}
25 id={image.id}
26 index={index}
27 moveImage={moveImage} // add this
28 />
29 ))
30 )}
31 </main>
32);
33};
34export default App;

The code above does the following:

  • We updated the Card component with the moveImage 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 the moveImage 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 <Card
66 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.

Resources

React Dnd

Christian Nwamba

Developer Advocate at AWS

A software engineer and developer advocate. I love to research and talk about web technologies and how to delight customers with them.