I am trying to upload multiple image to firebase cloud storage at once and after upload completes it gets the URL for all the images and push it to a list variable call urlsList then upload it to cloud fireStore. Every thing works up until the point where you should store the list of image urls to cloud fireStore.
In firestore it does not show the list of urls instead it shows an empty list, But when I console.log it shows the urlsList with all the urls in it,
Image of my database
Image of console.log
Here is an example of my code
const uplaodImage = async () => {
const promises = []
let urlsList = []
if (files.length > 0) {
setLoading(true)
files.map((file) => {
console.log('loop');
const storageRef = ref(storage, `juxtaposition/${currentUser?.uid}/${currentJob?.jobID}/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
promises.push(uploadTask)
uploadTask.on(
"state_changed",
(snapshot) => {
const prog = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgressPercent(prog);
},
(err) => dispatch(openSnackBar(err.message)),
async () => {
await getDownloadURL(uploadTask.snapshot.ref).then((downloadURLs) => {
urlsList.push(downloadURLs)
console.log("File available at", downloadURLs);
});
}
);
})
Promise.all(promises)
.then(async () => {
try {
console.log(urlsList)
const collectionRef = doc(db, "prevJobs", currentUser?.uid, "pendingJobs", currentJob?.jobID)
const deleteRef = doc(db, 'currentJobs', currentUser?.uid, 'yourJobs', currentJob?.jobID)
await setDoc(collectionRef, {
...currentJob,
juxtapositionImage: urlsList,
manufacturerID: currentUser?.uid,
status: 'pending'
})
deleteDoc(deleteRef)
} catch (err) {
setLoading(false)
dispatch(openSnackBar(err.message))
}
})
.then(() => {
setLoading(false)
closeDialog(false)
// navigate('/currentjobs', { replace: true })
})
} else {
setLoading(false)
setError(true)
}
}
CodePudding user response:
Overview
Because you are mixing the .on("state_changed", next, error, complete)
listener with the the Promise chaining syntax, your code is executing in a different order to what you expect. This is mainly because the chain hasn't been properly linked together.
When an upload completes, the getDownloadURL()
method is called - but because of the other uploads, it probably isn't being worked on yet and instead is added to the queue.
Once all three uploads complete, then the Promise.all(promises)
chain is executed. At this point, urlsList
is still empty which causes the the setDoc()
call to upload an empty array.
At this point, each of the getDownloadURL()
calls resolve, adding their respective URLs to urlsList
.
Now, when you open the console to inspect it, you see the urlsList
object, but in its current state, not in the state when it was actually logged. To do that, you need to clone the object as you log it using something like console.log(JSON.parse(JSON.stringify(urlsList)))
(it's a poor clone, but serves our purposes fine).
Correcting the Code
To start with, let's refactor these lines to a "fail fast" guard pattern as it will show the consequence right next to the problem:
if (files.length > 0) {
// lots of lines
} else {
setLoading(false)
setError(true)
}
becomes:
if (files.length == 0) {
setLoading(false)
setError(true)
return
}
// lots of lines
Additionally because your code is going to run into other problems when either currentUser?.uid
or currentJob?.jobID
return undefined
, you should add them to the guard too.
if (!currentUser || !currentJob || files.length == 0) {
setLoading(false);
setError(true); // <- consider passing a reason here rather than just true
return;
}
// pull out uid & jobID to eliminate currentUser?.uid and currentJob?.jobID everywhere
const { uid } = currentUser;
const { jobID } = currentJob;
Next, let's extract the logic used to upload the file, update progress and gets the download URL when done, in that order, returning a Promise once ALL of that is done.
const uploadTask = uploadBytesResumable(storageRef, file);
promises.push(uploadTask)
uploadTask.on(
"state_changed",
(snapshot) => {
const prog = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgressPercent(prog);
},
(err) => dispatch(openSnackBar(err.message)),
async () => { // <- onComplete must be synchronous - async/Promises will be floating
await getDownloadURL(uploadTask.snapshot.ref).then((downloadURLs) => {
urlsList.push(downloadURLs)
console.log("File available at", downloadURLs);
});
}
);
So that it uploads the file, updates the progress and gets the download URL, in order, and returns a Promise once ALL of that is done.
// place this outside of your component - saves rebuilding it multiple times
const doFileUpload = async (storageRef, file, setProgressPercent) {
console.log('Starting ' file.name ' upload...'); // informational
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const prog = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgressPercent(prog);
},
(err) => {
console.error('Upload of ' file.name ' failed.', err); // informational
// note: use uploadTask.catch() / doFileUpload.catch() for error handling
}
);
const uploadResult = await uploadTask; // wait for upload to resolve
const downloadURL = await getDownloadURL(uploadResult.ref);
setProgressPercent(100); // mark as complete
console.log('Uploaded ' file.name ' successfully...'); // informational
console.log('File available at ', downloadURL);
return downloadURL;
}
Next, you should update your "move job" operation so that it writes both changes at the same time (this helps the user resubmit the changes if something goes wrong):
const collectionRef = doc(db, "prevJobs", currentUser.uid, "pendingJobs", currentJob.jobID)
const deleteRef = doc(db, 'currentJobs', currentUser.uid, 'yourJobs', currentJob.jobID)
await setDoc(collectionRef, {
...currentJob,
juxtapositionImage: urlsList,
manufacturerID: currentUser?.uid,
status: 'pending'
})
deleteDoc(deleteRef)
becomes:
const batch = writeBatch(firestore);
// queue new document
const pendingJobsDocRef = doc(db, 'prevJobs', currentUser.uid, 'pendingJobs', currentJob.jobID)
batch.set(pendingJobsDocRef, {
...currentJob,
juxtapositionImage: urlsList,
manufacturerID: currentUser.uid,
status: 'pending'
})
// queue delete document
const yourJobsDocRef = doc(db, 'currentJobs', currentUser.uid, 'yourJobs', currentJob.jobID)
batch.delete(yourJobsDocRef);
// submit the changes
await batch.commit();
Reassembling the pieces, gives:
// Don't forget doFileUpload from above should be placed outside of your component
const uploadImage = () => { // <- note spelling correction
// fail fast first - handle all your problems here
if (!currentUser || !currentJob || files.length == 0) {
setLoading(false);
setError(true);
return;
}
// pull out uid & jobID to eliminate currentUser?.uid and currentJob?.jobID everywhere
const { uid } = currentUser;
const { jobID } = currentJob;
setLoading(true);
// upload all files
const promises = files.map((file, idx) => {
console.log('Starting upload of files[' idx ']'); // informational
const storageRef = ref(storage, `juxtaposition/${uid}/${jobID}/${file.name}`);
return doFileUpload(storageRef, file, setProgressPercent);
});
return Promise.all(promises)
.then((urlsArray) => {
// move job from currentJobs to prevJobs
const pendingJobsDocRef = doc(db, 'prevJobs', uid, 'pendingJobs', jobID),
yourJobsDocRef = doc(db, 'currentJobs', uid, 'yourJobs', jobID);
return writeBatch(firestore)
.set(pendingJobsDocRef, {
...currentJob,
juxtapositionImage: urlsList,
manufacturerID: uid,
status: 'pending'
})
.delete(yourJobsDocRef);
.commit();
})
.then(() => {
// all uploads completed, and database has been updated
setLoading(false);
closeDialog(false);
})
.catch((err) => {
// one or more uploads failed
console.error('One or more uploads failed: ', err); // log for diagnosis rather than just ignoring it
setLoading(false);
setError(true);
})
}