I am using React and Firebase. I am trying to store the download url of an image to a state called ‘image’, and then using that state to add to the ‘imageurl’ field in the post being created.
It seems there is no problem with updating the state of the image, but when I look at the ‘imageurl’ field in the new post object created in my firebase database, it is empty. Here’s the code:
import { storage, db } from '../firebase-config';
import { useState } from 'react';
import { ref, getDownloadURL, uploadBytesResumable } from "firebase/storage";
import { collection, addDoc } from 'firebase/firestore';
import { format } from 'date-fns';
const CreatePost = () => {
const [caption, setCaption] = useState('');
const [progress, setProgress] = useState(0);
const [image, setImage] = useState('');
const postsCollectionRef = collection(db, 'Posts');
const formHandler = (e) => {
e.preventDefault();
const file = e.target[0].files[0];
uploadFiles(file);
createPost();
};
const uploadFiles = (file) => {
if (!file) return;
const storageRef = ref(storage, `files/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const prog = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgress(prog);
},
(error) => console.log(error),
() => {
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
setImage(downloadURL);
});
}
);
};
const createPost = async () => {
const datetime = format(new Date(), 'MMMM dd, yyyy');
const post = await addDoc(postsCollectionRef, {
caption: caption,
imageurl: image,
date: datetime
})
setCaption('');
}
return (
<div>
<form onSubmit={formHandler}>
<input type="file" className="input" />
<button type="submit">Upload</button>
<input value={caption} type='text' placeholder='caption' onChange={(e) => setCaption(e.target.value)} />
</form>
<hr />
<h2>Uploading done {progress}%</h2>
<img src={image} />
<p>{image}</p>
</div>
);
}
export default CreatePost;
>Solution :
You should use the state to trigger updates to the UI, and use other synchronization primitives to determine when to update the database.
For example, at its simplest, you can call createPost from within the nested listener when you got the download URL:
const formHandler = (e) => {
e.preventDefault();
const file = e.target[0].files[0];
uploadFiles(file);
// createPost(); // 👈 don't call this here
};
const uploadFiles = (file) => {
if (!file) return;
const storageRef = ref(storage, `files/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const prog = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgress(prog);
},
(error) => console.log(error),
() => {
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
setImage(downloadURL);
createPost(); // 👈 but instead call it here
});
}
);
};
You may need to pass the downloadURL and other values you want to write to the data into the createPost call, which is another case of not using React’s state to manage your database updates.
Alternatively, you can use Promises and/or async/await to synchronize the calls.
const formHandler = (e) => {
e.preventDefault();
const file = e.target[0].files[0];
const url = await uploadFiles(file); // 👈 wait for upload to be done
createPost(url); // 👈 only then create the post with the url
};
// 👇 this function is asynchronous, meaning it returns a promise
const uploadFiles = (file) => async {
if (!file) return;
const storageRef = ref(storage, `files/${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
"state_changed",
(snapshot) => {
const prog = Math.round(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
setProgress(prog);
},
(error) => console.log(error)
);
await uploadTask; // 👈 uploadTask is a promise itself, so you can await it
let downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
// 👆 getDownloadURL returns a promise too, so... yay more await
return downloadURL; // 👈 return the URL to the caller
};