Skip to content

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

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.

Create a new React app using the following command:

    npx 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:

    npm i antd @ant-design/icons
Code language: CSS (css)

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

    @import '~antd/dist/antd.css';
Code language: CSS (css)

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:

    import { useState } from 'react';
    const useFileSelection = () => {
      const [selectedFiles, setSelectedFiles] = useState([]);
      const addFile = (file) => {
        setSelectedFiles((currentSelection) => [...currentSelection, file]);
      };
      const removeFile = (file) => {
        setSelectedFiles((currentSelection) => {
          const newSelection = currentSelection.slice();
          const fileIndex = currentSelection.indexOf(file);
          newSelection.splice(fileIndex, 1);
          return newSelection;
        });
      };
      return [addFile, removeFile];
    };
    export default useFileSelection;
Code language: JavaScript (javascript)

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.

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

    import { Modal } from 'antd';
    import { useState } from 'react';
    const useFilePreview = () => {
      const [previewVisibility, setPreviewVisibility] = useState(false);
      const [previewImage, setPreviewImage] = useState(null);
      const [previewTitle, setPreviewTitle] = useState('');
      const getBase64Representation = (file) =>
        new Promise((resolve, reject) => {
          const reader = new FileReader();
          reader.readAsDataURL(file);
          reader.onload = () => resolve(reader.result);
          reader.onerror = (error) => reject(error);
        });
      const handlePreview = async (file) => {
        if (!file.url && !file.preview) {
          file.preview = await getBase64Representation(file.originFileObj);
        }
        setPreviewImage(file.url || file.preview);
        setPreviewVisibility(true);
        setPreviewTitle(
          file.name || file.url.substring(file.url.lastIndexOf('/') + 1)
        );
      };
      const hidePreview = () => {
        setPreviewVisibility(false);
      };
      const previewContent = (
        <Modal
          visible={previewVisibility}
          title={previewTitle}
          footer={null}
          onCancel={hidePreview}
        >
          <img alt={previewTitle} style={{ width: '100%' }} src={previewImage} />
        </Modal>
      );
      return [handlePreview, previewContent];
    };
    export default useFilePreview;
Code language: JavaScript (javascript)

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.

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

    import { Upload } from 'antd';
    import { PlusOutlined } from '@ant-design/icons';
    import useFilePreview from '../hooks/useFilePreview';
    const { Dragger } = Upload;
    const DragAndDrop = ({ addFile, removeFile }) => {
      const [handlePreview, previewContent] = useFilePreview();
      const beforeUploadHandler = (file) => {
        addFile(file);
        return false;
      };
      return (
        <>
          <Dragger
            multiple={true}
            onRemove={removeFile}
            showUploadList={true}
            listType="picture-card"
            beforeUpload={beforeUploadHandler}
            onPreview={handlePreview}
            accept="image/*"
          >
            <p className="ant-upload-drag-icon">
              <PlusOutlined />
            </p>
            <p className="ant-upload-text">
              Click this area or drag files to upload
            </p>
          </Dragger>
          {previewContent}
        </>
      );
    };
    export default DragAndDrop;
Code language: JavaScript (javascript)

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.

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

    import './App.css';
    import { Button, Card } from 'antd';
    import DragAndDrop from './components/DragAndDrop';
    import useFileSelection from './hooks/useFileSelection';
    const App = () => {
      const [addFile, removeFile] = useFileSelection();
      return (
        <div style={{ margin: '1%' }}>
          <Card
            style={{ margin: 'auto', width: '50%' }}
            actions={[<Button type="primary">Submit</Button>]}
          >
            <DragAndDrop addFile={addFile} removeFile={removeFile} />
          </Card>
        </div>
      );
    };
    export default App;
Code language: JavaScript (javascript)

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:

    npm 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.

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.

Back to top

Featured Post