React multi-file upload with add or remove option

by Vincy. Last modified on February 11th, 2024.

Multi-file upload allows user to send more than one file to server in an operation. It helps to give better user experience.

In the last React article, we created a multi-file upload component with a progress bar. It used the default behaviour of holding Ctrl+ Or Command to choose multi-files.

View Demo

This article supports multi-file upload using React with add more option. It allows adding or removing multiple file inputs to the UI. The advantage of this feature is the “remove” option that helps to clear a particular file from the chosen file input array.

It has both client-side and server-side file validation before processing the upload.

Multi File Upload in React Output

HTML view to render React Multi-file upload component

In a landing page, there will be only one file input shown to the user initially. It will show an “Add more” link to append more file inputs dynamically.

The React component root is enclosed by a form to post the uploaded files with a formData object via Axios.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="css/style.css" />
  <link rel="stylesheet" href="css/form.css" />
  <title>React multi-file upload with add or remove option</title>
</head>

<body>
  <h1>React multi-file upload with add or remove option</h1>
  <form id="from-image-upload">
    <div id="multiple-file-upload-component"></div>
  </form>
  
</body>

</html>

React script imports and building UI components

The below React script has required imports and functions to deploy UI components on an event basis.

This section has two React components.

  1. File input component which can be deployed on the UI on clicking “Add more”.
  2. Multi-file upload component that returns the “Upload” button control.

The File-input component “on-change” event is mapped to validate the uploaded file type and size.

The Multi-file upload component maps the “on-submit” event to request the backend process to upload selected files.

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="add-more.js"></script>
<script type="text/babel">
  const { useState } = React;
  const { render } = ReactDOM;

  const FileInput = ({ index, files, setFilesChange, setErrorMessage }) => {
    const handleFileOnChange = (e) => {
      const formFileData = Array.from(e.target.files);
      var validFiles = validateFileOnChange(formFileData);
      if (validFiles.length > 0) {
        validFiles.forEach((file) => {
          setFilesChange(validFiles, file, index);
        });
        setErrorMessage("");
      } else {
        const errorMessage = `Invalid files: Please select only image files (jpeg, png, gif) with size less than 2MB.`;
        setErrorMessage(errorMessage);
      }
    };

    return (
      <div className="row inline-block"><input type="file" name="file[]" multiple accept="image/jpeg, image/png, image/gif" onChange={handleFileOnChange} /></div>
    )
  }

  const MultiFileUpload = () => {
    const [files, setFiles] = useState([]);
    const [errorMessage, setErrorMessage] = useState("");
    const [uploadButtonClicked, setUploadButtonClicked] = useState(false);
    const initFileState = () => {
      setFiles([]);
      setFiles((prevFiles) => [...prevFiles, null]);
    };

    // Update the state with the new file at the specified index
    const setFilesChange = (files, file, index) => {
      setFiles((prevFiles) => {
        const files = [...prevFiles];
        files[index] = file;
        return files;
      });
    }

    const handleMultiFileUpload = async () => {
      var error = validateFileOnUpload(files);
      if (error) {
        setErrorMessage(error);
        return false;
      }
      setErrorMessage("");
      setUploadButtonClicked(true);

      handleMultipleFileUploadAjax(initFileState);
    };

    const handleAddMore = () => {
      setFiles((prevFiles) => [...prevFiles, null]);
    };

    const handleRemoveFile = (event, index) => {
      const updatedFiles = [...files];
      updatedFiles.splice(index, 1);
      setFiles(updatedFiles);
    };
    return (
      <div>
        {errorMessage && <p className="validation-message">{errorMessage}</p>}
        <div className="button-container">
          {files && files.map((file, index) => (
            <div key={index}>
              <FileInput index={index} files={files} setFilesChange={setFilesChange} setErrorMessage={setErrorMessage} />
              {file && (
                <>
                  <span onClick={() => handleRemoveFile(event, index)} className="remove-link"> Remove</span>
                </>
              )}
            </div>
          ))}
          <div id="add-more" className="row" onClick={handleAddMore}>Add more</div>
          <div className="row"><button type="button" onClick={handleMultiFileUpload}>Upload</button></div>
        </div>
        {uploadButtonClicked && (
          <div>
            <div className="image-container">
              {files.map((file, index) => (
                file && (
                  <div key={index} className="image-preview">
                    <div>
                      <img className="preview" src={URL.createObjectURL(file)} alt="Preview" />
                    </div>
                  </div>
                )
              ))}
            </div>
          </div>
        )}
      </div>
    );
  };

  ReactDOM.render(<MultiFileUpload />, document.getElementById("multiple-file-upload-component"));

  document.getElementById("add-more").click();
</script>

JavaScript functions for validation and remove file-input

The below JavaScript file consists of functions to validate the uploaded file type and size on choosing the file.

The validateFileOnChange function sets supported file types for this React example and restrict the front end users from uploading wrong files.

On clicking “add-more”, the handleAddMore() function adds a “remove” link to the deployed file-input element. This will help to clear a particular file-input instance from the multi-file upload unit.

Once all files are uploaded to the server, then the remove links shown on the UI will all be cleared.

The handleMultipleFileUploadAjax function fires file-upload request via axios. This is to make this example as an AJAX-powered multi-file upload.

add-more.js

function validateFileOnChange(formFileData) {
  const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
  const maxSize = 2 * 1024 * 1024; // 2MB

  let validFiles = [];

  formFileData.forEach((file) => {
    if (allowedTypes.includes(file.type) && file.size <= maxSize) {
      validFiles.push(file);
    }
  });

  return validFiles;
}

function validateFileOnUpload(files) {
  var error = "";
  if (files.length === 0) {
    error = "Please select at least one file to upload.";
  }

  var totalSize = 0;
  for (let i = 0; i < files.length; i++) {
      const file = files[i];
      if(file) {
        totalSize += parseInt(file.size)
      }
  }
  const maxSize = 2 * 1024 * 1024; // 2MB

  if (totalSize > maxSize) {
    error = "Total size of selected images exceeds 2MB.";
  }
  return error;
}

async function handleMultipleFileUploadAjax(initFileState) {
  try {
    const formData = new FormData(document.getElementById("from-image-upload"));

    // Send files binary to PHP
    axios.post('upload-image.php', formData, { function() {
    }
    }).then(response => {
      console.log(response.data);
    });
  } catch (error) {
    console.error(error);
  }
}

PHP multi-file upload code to save files to server

This is an usual file upload script that we have seen in the previous tutorials. If you are searching for a multi-file upload script with Dropzone and PHP, the the link has the code.

After uploading the files, the uploaded files userState is reset in the Axios callback. In the above script, the initFileState() function empty the files array and put it into the initial state.

upload-image.php

<?php
$responseMessage = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    try {
        if (isset($_FILES['file'])) {
            $uploadDirectory = __DIR__ . '/uploads/';

            $uploadedFiles = [];
            $filesCount = count($_FILES['file']['name']);
            for ($i = 0; $i < $filesCount; $i++) {

                $fileName = $_FILES['file']['name'][$i];
                $fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
                $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];

                if (in_array($fileExtension, $allowedExtensions)) {
                    $newFilename = uniqid() . '.' . $fileExtension;
                    $uploadPath = $uploadDirectory . $newFilename;

                    if (move_uploaded_file($_FILES['file']['tmp_name'][$i], $uploadPath)) {
                        $uploadedFiles[] = $newFilename;
                    } else {
                        $responseMessage = ['error' => 'Upload failed.'];
                        exit;
                    }
                } else {
                    $responseMessage = ['error' => 'Invalid file extension.'];
                    exit;
                }
            }
        } else {
            $responseMessage = ['error' => 'No files found to upload.'];
        }
    } catch (Exception $e) {
        $responseMessage = ['error' => $e->getMessage()];
    }
} else {
    $responseMessage = ['error' => 'Invalid request.'];
}
echo json_encode($responseMessage);
exit;
?>

View Demo Download

Vincy
Written by Vincy, a web developer with 15+ years of experience and a Masters degree in Computer Science. She specializes in building modern, lightweight websites using PHP, JavaScript, React, and related technologies. Phppot helps you in mastering web development through over a decade of publishing quality tutorials.

Leave a Reply

Your email address will not be published. Required fields are marked *

↑ Back to Top

Share this page