Add Drag and Drop for Images with React (1/2)

Ifeoma Imoh

As the world becomes increasingly interconnected, file sharing and exchange (mainly images) are becoming more prevalent. As a result, particular care must be taken in making the process as easy as possible.

This is the first part of a two-part series. In this post, we’ll look at how to add drag and drop functionality for media files to a React application, allowing you to select multiple images at once and, not only that, preview the selected images before uploading. Before uploading, you will also have the opportunity to delete an image if you wish to.

Here's a link to the demo on CodeSandbox.

Project Setup

Create a new React app using the following command:

1npx create-react-app drag_and_drop_demo

Next, let's add the project dependencies. We will be using Ant Design to render our UI components for this project. Add it to your project using the following command:

1npm i antd @ant-design/icons

Next, we need to import the antd CSS. To do this, open src/App.css and edit its content to match the following:

1@import '~antd/dist/antd.css';

Implement Drag and Drop Functionality

Create useFileSelection Hook

To keep our components lean and focused on presentation, we’ll use hooks to inject the needed functionality into the component. In the src folder, create a new folder called hooks. This folder will hold the hooks we will create in this app - one for the drag and drop functionality and the other for preview.

In the src/hooks folder, create a new file called useFileSelection.js and add the following to it:

1import { useState } from 'react';
2 const useFileSelection = () => {
3 const [selectedFiles, setSelectedFiles] = useState([]);
4 const addFile = (file) => {
5 setSelectedFiles((currentSelection) => [...currentSelection, file]);
6 };
7 const removeFile = (file) => {
8 setSelectedFiles((currentSelection) => {
9 const newSelection = currentSelection.slice();
10 const fileIndex = currentSelection.indexOf(file);
11 newSelection.splice(fileIndex, 1);
12 return newSelection;
13 });
14 };
15 return [addFile, removeFile];
16 };
17 export default useFileSelection;

We start by declaring a state variable for the selected files. Since we will be dealing with more than one file in the selection, we initialized it with an empty array.

Next, we declare a function named addFile to add a file to the array of selected files. The function takes a file and adds it to the currently selected files by using the setSelectedFiles function to update the state.

We also want to be able to remove a file from the list of selected files, so we declare a function named removeFile, which accepts the file to be removed as a parameter. In the setSelectedFiles function call, we pass a function that takes the selected files in state and does the following:

  1. Creates a shallow copy of the current selection of files using the slice function.
  2. Gets the index of the provided file in the current selection of files.
  3. Removes the file from the shallow copy using the splice function.
  4. Returns the new selection with the file removed.

Finally, we return an array containing the addFile and removeFile functions.

Create useFilePreview Hook

In the src/hooks folder, create a new file named useFilePreview.js and add the following to it:

1import { Modal } from 'antd';
2 import { useState } from 'react';
3 const useFilePreview = () => {
4 const [previewVisibility, setPreviewVisibility] = useState(false);
5 const [previewImage, setPreviewImage] = useState(null);
6 const [previewTitle, setPreviewTitle] = useState('');
7 const getBase64Representation = (file) =>
8 new Promise((resolve, reject) => {
9 const reader = new FileReader();
10 reader.readAsDataURL(file);
11 reader.onload = () => resolve(reader.result);
12 reader.onerror = (error) => reject(error);
13 });
14 const handlePreview = async (file) => {
15 if (!file.url && !file.preview) {
16 file.preview = await getBase64Representation(file.originFileObj);
17 }
18 setPreviewImage(file.url || file.preview);
19 setPreviewVisibility(true);
20 setPreviewTitle(
21 file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
22 );
23 };
24 const hidePreview = () => {
25 setPreviewVisibility(false);
26 };
27 const previewContent = (
28 <Modal
29 visible={previewVisibility}
30 title={previewTitle}
31 footer={null}
32 onCancel={hidePreview}
33 >
34 <img alt={previewTitle} style={{ width: '100%' }} src={previewImage} />
35 </Modal>
36 );
37 return [handlePreview, previewContent];
38 };
39 export default useFilePreview;

In this hook, we declare three state variables. previewVisibility is a boolean that determines whether the image preview should be rendered. previewImage contains the base64 representation of the image to be previewed, while previewTitle contains the title of the preview, which is the name of the file to be previewed.

Next, we declare a function named getBase64Representation which takes a file and returns its base64 representation.

After this, we declare an asynchronous function named handlePreview which takes a file and updates the preview image, title, and visibility in state. By setting the visibility to true, the preview will be rendered and displayed to the user.

The user should also have the option to close the preview. The hidePreview function handles this by setting the visibility in state to false, causing the preview to be hidden. Next, we declare the JSX for the preview. The preview is essentially an image wrapped in a Modal component. Using the title and image stored in state, we update the relevant props for the preview component. Finally, we return an array containing the handlePreview function and previewContent constant.

With the hooks in place, we can create the components for our application. In the src folder, create a new folder called components.

Create DragAndDrop Component

In the src/components directory, create a new file called DragAndDrop.js and add the following to it:

1import { Upload } from 'antd';
2 import { PlusOutlined } from '@ant-design/icons';
3 import useFilePreview from '../hooks/useFilePreview';
4 const { Dragger } = Upload;
5 const DragAndDrop = ({ addFile, removeFile }) => {
6 const [handlePreview, previewContent] = useFilePreview();
7 const beforeUploadHandler = (file) => {
8 addFile(file);
9 return false;
10 };
11 return (
12 <>
13 <Dragger
14 multiple={true}
15 onRemove={removeFile}
16 showUploadList={true}
17 listType="picture-card"
18 beforeUpload={beforeUploadHandler}
19 onPreview={handlePreview}
20 accept="image/*"
21 >
22 <p className="ant-upload-drag-icon">
23 <PlusOutlined />
24 </p>
25 <p className="ant-upload-text">
26 Click this area or drag files to upload
27 </p>
28 </Dragger>
29 {previewContent}
30 </>
31 );
32 };
33 export default DragAndDrop;

The DragAndDrop component takes two props - addFile and removeFile. These will be used to update the selection of files in state. The functionality and JSX to render previews of uploaded images are retrieved from the useFilePreview hook.

The beforeUploadHandler function takes a file and adds it to the selection in state. This function is passed as a prop to override the default behavior of the Upload component when a file is added. By returning false, we disable the default behavior of uploading the selected file to a provided URL.

Finally, we return the JSX for the DragAndDrop component. Antd provides a Dragger component as part of the Upload component. Because we want to upload multiple images at once, we pass true for the multiple prop. We also restrict the accepted file type to images using the accept prop. The showUploadList and list-type props let the component know that we want to display the uploaded images as a list with picture-card, causing the list to be displayed as a grid of thumbnail images. We also include the JSX for the image preview (retrieved from the useFilePreview hook) under the Dragger component.

Putting It All Together

Update your src/App.js file to match the following:

1import './App.css';
2 import { Button, Card } from 'antd';
3 import DragAndDrop from './components/DragAndDrop';
4 import useFileSelection from './hooks/useFileSelection';
5 const App = () => {
6 const [addFile, removeFile] = useFileSelection();
7 return (
8 <div style={{ margin: '1%' }}>
9 <Card
10 style={{ margin: 'auto', width: '50%' }}
11 actions={[<Button type="primary">Submit</Button>]}
12 >
13 <DragAndDrop addFile={addFile} removeFile={removeFile} />
14 </Card>
15 </div>
16 );
17 };
18 export default App;

In the App component, we retrieve the addFile and removeFile functions from the useFileSelection hook, which we pass as props to the DragAndDrop component. We wrap the DragAndDrop component in a Card component. We also include a button in the card, which will be used to submit (upload) the selected files.

Run this command to spin up a local development server:

1npm start

By default, the application will be available at http://localhost:3000/. When you upload some images, the final result should look like the image below:

Find the complete project here on GitHub.

Conclusion

In this article, we saw how to take advantage of Antd to create a visually appealing component that eases the process of uploading multiple images via the drag and drop functionality. In the next part of this series, we'll take things a step further by uploading multiple files to Cloudinary at once.

Here are some resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.