I built an uploader element that accepts multiple files and then uploads them (max 3 at a time). Unfortunately it turned out to be a big mess. I get key conflicts and a bunch of wacky behaviors. I think the problem might lie in the async updating of the "uploads" state. Every uploading element frequently updates the shared "uploads" state with the current progress. I'd appreciate if anyone could look over this and point out improvements to the structure.
The upload element has one prop, which is an array that takes in the files from the input element. After the props have been passed, I clear the form on the parent.
I am using two states on the uploader. "open" controls the visibility of the element and "uploads" tracks all files.
const [open, setOpen] = useState(true);
const [uploads, setUploads] = useState([]);
When new files are being passed down, I add various values to them for keeping track of the file's status and then add them to "uploads".
// Push to queue
useEffect(() => {
if (files.length === 0) return;
setUploads((prev) => {
const newArray = [...prev];
// Add data to each file
files.forEach((file) => {
const id = uuidv4();
const newFile = {
active: false,
file: file,
id: id,
name: file.name,
progress: 0,
push: false,
};
newArray.push(newFile);
});
return [...newArray];
});
}, [files]);
Every time "uploads" changes, the following effect is triggered. Its supposed to enforce the max number of simultaneous uploads and filter for files that are not yet uploading.
// Activate upload
useEffect(() => {
// Prevent running on empty array
if (uploads.length === 0) return;
// Limit number of uploads
const activeUploads = uploads.filter((el) => el.active === true);
if (activeUploads.length > 2) return;
// Get first file from queue
const file = uploads.find((el) => el.active === false);
if (file === undefined) return;
// Prevent duplicates
//const duplicates = uploading.filter((el) => el.id === file.id);
//if (duplicates.length > 0) return;
// Add xhr to file
createXhr(file).then((xhr) => {
// Append xhr to element
file.xhr = xhr;
// Activate
file.active = true;
// Get index
const i = uploads.findIndex((el) => el.id === file.id);
//Push to active Uploads
setUploads((prev) => {
prev[i] = file;
return [...prev];
});
// Start upload
xhr.send(file.file);
});
}, [uploads]);
FYI, here is the function for adding the xhr object:
// Create xhr
async function createXhr(fileObject) {
// Get file from file object
const file = fileObject.file;
// Get file id
const fileId = fileObject.id;
// Get upload url
const { url, uuid, key, name } = await getSignedUploadUrl({
name: file.name,
type: file.type,
});
// Create XHR object
const xhr = new XMLHttpRequest();
// onProgress
xhr.upload.onprogress = (e) => {
// Get progress
const percentage = (e.loaded / e.total) * 100;
//Push to active Uploads
updateUpload(fileId, "progress", percentage);
};
// onError
xhr.onerror = () => {
xhr.abort();
};
// onAbort
xhr.onabort = () => {
removeUpload(fileId);
};
// onSuccess
xhr.onload = async () => {
// Set database
const res = await checkWasabiFile(key);
// Proceed if file exists
if (res.data) {
// Get download url
const urlObject = await firebase
.functions()
.httpsCallable("sign_wasabi_download_url")({
storage_key: key,
});
// Create Firestore object
await firebase
.firestore()
.collection("users")
.doc(user.uid)
.collection("files")
.doc(uuid)
.set({
storage_key: key,
name: name.split(".")[0],
owner: user.uid,
path: "/",
suffix: key.split(".")[1],
tags: [],
type: file.type.split("/")[0],
url: urlObject.data,
});
// Get thumbnails
if (file.type.split("/")[0] === "image") {
//Image
await fetch(
`https://api.cardboard.video/img-thumb-${env}?key=${key}`
);
} else if (file.type.split("/")[0] === "video") {
//Video
fetch(`https://api.cardboard.video/video-thumb-${env}?key=${key}`);
}
} else {
window.alert("There was a problem with your upload. Please try again.");
}
// Remove from uploads
removeUpload(fileId);
};
xhr.open("PUT", url, true);
xhr.setRequestHeader("Content-Type", file.type);
return xhr;
}
Here is the function for updating the state on progress and removing the element on load:
// Remove item from upload
const removeUpload = (id) => {
// Remove from uploads
setUploads((prev) => {
const items = [...prev];
const index = items.findIndex((el) => el.id === id);
items.splice(index, 1);
return [...items];
}, console.log(uploads));
};
// Update upload object
const updateUpload = (id, key, value) => {
// Prevent execution on empty array
if (uploads.length === 0) return;
// Update upload state
setUploads((prev) => {
const items = [...prev];
const i = items.findIndex((el) => el.id === id);
if (items[i] === undefined) return [...prev];
items[i][key] = value;
return [...items];
});
};
I appreciate any suggestions.
question from:
https://stackoverflow.com/questions/65846678/best-practices-for-react-uploader