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.
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.
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>
The below React script has required imports and functions to deploy UI components on an event basis.
This section has two React components.
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>
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);
}
}
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;
?>