I'm new to react and Firebase.
I'm building an app where users can upload several big images(up to 3MB) and videos to Firebase storage and then reference url in Firestore. Both are submitted with one onSubmit function. Everything else works, as in, users are able to drop, see which images are accepted, press submit and the images are uploaded to Firebase storage and Firestore.
Its just that the setIsUploading(true) does not work once submit button is pressed. All my console.logs come back false throughout the code.
While trying to find a fix, i learnt that useState is asynchronous and useEffect would resolve this but i don't know how to put it in the code and what would be its dependency/dependencies.
//React
import { useEffect, useState } from "react";
//Firebase
import { storage } from "../firebase";
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
import { auth, db } from "../firebase";
import { serverTimestamp, setDoc, doc } from "firebase/firestore";
//imports from video and image from components
import ImageUpload from "../components/portfolioComponents/ImageUpload";
import VideoUpload from "../components/portfolioComponents/VideoUpload";
//Others
import { toast } from "react-toastify";
import { v4 as uuidv4 } from "uuid";
import Loading from "../components/Loading";
export default function PortfolioEdit() {
//identify current user
const user = auth.currentUser;
const [isUploading, setIsUploading] = useState(false);
const [images, setImages] = useState([]);
const [videos, setVideos] = useState([]);
const onSubmit = (e) => {
e.preventDefault();
setIsUploading(true)
if (videos.length === 0 && images.length === 0) {
toast.error("Please upload images and/or videos");
return
}
if (images.length > 0) {
images.map((image) => {
setIsUploading(true);
const storageRef = ref(
storage,
"images/" + user.uid + "/" + uuidv4() + "-" + image.name
);
const uploadTask = uploadBytesResumable(storageRef, image);
uploadTask.on(
"state_changed",
(snapshot) => {
// Observe state change events such as progress, pause, and resume
// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log(
image.name + ": Upload is " + Math.ceil(progress) + "% done"
);
switch (snapshot.state) {
case "paused":
console.log("Upload is paused");
break;
case "running":
console.log("Upload is running");
break;
}
},
(error) => {
// Handle unsuccessful uploads
toast.error(error.message);
console.log(error.message);
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
console.log("File available at", downloadURL);
console.log(user.uid);
console.log("Upload Status: " + isUploading);
try {
setDoc(doc(db, "images/" + user.uid), {
imageURL: downloadURL,
createdAt: serverTimestamp(),
user: user.uid,
});
toast.success("Your image has been added");
} catch (error) {
console.log(error);
toast.error(error.message);
}
});
}
);
});
}
setIsUploading(false)
};
return (
<>
{isUploading ? (
<Loading />
) : (
<>
<h1 className="text-center mt-10">
Add Images and Videos to your portfolio
</h1>
<form onSubmit={onSubmit}>
<ImageUpload setImages={setImages} />
<VideoUpload setVideos={setVideos} />
<button className="block mx-auto mt-4 p-4 border-2 border-black">
Upload Images
</button>
</form>
<p>Loading status: {isUploading}</p>
</>
)}
</>
);
}
I would really appreciate this. I've been struggling with this for a while.
Well while the setIsUploading is async operation, it's really fast and it doesn't look like the issue here.
I think your issue is that you are not waiting for anything before setting the isUploading back to false (so you are not waiting for any of the uploads to finish, which are async).
You basically need to wait for all async operations to end if you want your isUploading to properly reflect the state of uploading images. One way to accomplish this is something like:
const onSubmit = async (e) => {
e.preventDefault()
setIsUploading(true)
// 1. Create an array to put promises to await for
const imageLoadingPromises = []
...
if (images.length > 0) {
images.map((image) => {
// 2. Create a promise for each image upload
const uploadPromise = new Promise((resolve) => {
const storageRef = ref(
storage,
"images/" + user.uid + "/" + uuidv4() + "-" + image.name
)
const uploadTask = uploadBytesResumable(storageRef, image)
uploadTask.on(
"state_changed",
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
switch (snapshot.state) {
...
}
},
(error) => {
toast.error(error.message)
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
try {
setDoc(doc(db, "images/" + user.uid), {
imageURL: downloadURL,
createdAt: serverTimestamp(),
user: user.uid,
})
toast.success("Your image has been added")
} catch (error) {
toast.error(error.message)
}
// 3. resolve the promise once the loading is done
resolve()
})
}
)
})
// 4. Add the promise to the array imageLoadingPromises array
imageLoadingPromises.push(uploadPromise)
})
}
// 5. Wait for all images to be loaded before setting the isUploading back to false
await Promise.all(imageLoadingPromises)
setIsUploading(false)
}
So basically adding the 5 steps mentioned above it your code should help you fixing your issue. Please note that this is a proof of concept. So if there is a case where you don't get to the resolve(), or you encounter other minor issues you might need to adapt it to your needs.
TIPS:
you don't really need setIsUploading(true); inside images.map since it should already be set to true at the beginning of the function
Also splitting your long onSubmit function into smaller, meaningful named functions would help a lot.
Set it to true as soon as your function initiates but than you have to set it false at uploadTask on callback.
Since that callback is also async is just finishing later than the false setter.
Related
Edit - added minimally reproducible example: https://snack.expo.dev/#hdorra/code
I hope everyone can access the snack. So if you add a task, you can see it show up in the log. Click on the circle, it shows as true (meaning it is clicked). Save and refresh and everything is stored (the task) but the checkbox is not. I stripped the code to make it as bare minimum as possible but it shows the problem.
It has been days of me on this error. I am relatively new to stackoverflow so my apologies if my question isn't clear or I am not asking it in the correct format. I am trying to create a to do app in react native that is using async storage. I created a toggle button that saves the toggle to a state. This button is located in a component:
const [checkBoxState, setCheckBoxState] = React.useState(false);
const toggleComplete = () => {
setCheckBoxState(!checkBoxState)
handleEdit();
console.log(checkBoxState)
}
When the user checks on it - seems to be showing up correctly as marked true and false in the console.
Then, this is passed to an edit handler to update the array, again console shows it is the correct state:
const handleEdit = () => {
props.editHandler(props.todoKey, text, checkBoxState);
console.log(text2, checkBoxState)
};
Then it shows that it saved correctly:
const [todos, setTodos] = React.useState([]);
const handleEdit = (todoKey, text, newStatus) => {
const newTodos = [...todos];
const index = newTodos.findIndex(todos => todos.key === todoKey);
newTodos[index] = Object.assign(newTodos[index], {title: text, status: newStatus});
setTodos(newTodos);
console.log(todos, newStatus)
};
The async function to save to the device and load are as follows:
To save:
const saveTodoToUserDevice = async (todos) => {
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem('todos', stringifyTodos);
} catch (error) {
console.log(error);
}
};
To load from the device:
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem('todos');
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
}
};
So here is the issue - I get the console log that says it is saved correctly and loaded. BUT, when I refresh, the checkbox state is not saved at all, just the title text (so it is saving but the checkbox would always be false (the initial state set). If I clicked on true, it would show as true and then when I refresh, it goes back to false.
I have spent days and days on this and can't figure it out. Any direction would be helpful Thank you!
I have gone through your code and found some errors you are making in different places. In Task.js you can do without that checkBoxState. For that, pass the status to Task as props while rendering it in FlatList, like so:
<Task
key={item.key}
todoKey={item.key}
title={item.title}
status={item.status}
editHandler={handleEdit}
pressHandler={handleDelete}
/>
Then as below, change the button to toggle the status, so you use what's coming from the props and create a function called toggleStatus and pass it to onPress:
<TouchableOpacity onPress={toggleStatus}>
<View
style={[
styles.circle,
!props.status ? styles.completeCircle : styles.incompleteCircle,
]}
></View>
</TouchableOpacity>
The code for toggleStatus:
const toggleStatus = () => {
props.editHandler(props.todoKey, props.title, !props.status);
};
And handleEdit would be simplified to:
const handleEdit = () => {
props.editHandler(props.todoKey, text2, props.status);
setEdit(false);
console.log(props.status);
};
Lastly, in TasksMain.js so you don't replace what's in the storage with that initial array given to useState, make sure saveTodoToUserDevice runs after getTodosFromUserDevice. For that, add the below state in TasksMain.js and slightly change the two functions as follow:
const [loading, setLoading] = React.useState(true);
const saveTodoToUserDevice = async (todos) => {
if (loading) return;
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem("todos", stringifyTodos);
} catch (error) {
console.log(error);
}
};
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem("todos");
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
I'm reading from my firebase realtime database when my component loads to update a state with the returned data. Whenever the component loads, it gets the data from the database 8 times, and this causes React to give an error since the state is updated too many times in quick succession.
I've tried using both the onValue listener and the get function, and they both do the same thing on page load. If instead, I do not call them on page load, and either onValue or get runs manually or from the database being updated, it runs only once as expected.
The component:
export default function FoodItem(props) {
const [foods, setFoods] = useState()
const db = getDatabase()
// creating the onValue listener (below) creates the same issue
// const mealRef = ref(db, (props.userId + "/" + props.date + "/" + props.meal))
// onValue(mealRef, (snapshot) => {
// const data = snapshot.val()
// console.log(data)
// setFoods(data)
// })
useEffect(() => {
console.log("getFoods")
const dbRef = ref(getDatabase())
get(child(dbRef, (props.userId + "/" + props.date + "/" + props.meal))).then((snapshot) => {
if (snapshot.exists()) {
console.log(snapshot.val())
// setFoods(data)
} else {
console.log("No data available")
}
}).catch((error) => {
console.error(error)
})
}, [])
return (
...
)
}
The console output from the code above
If instead of using useEffect, I call get with a function (for example, with onClick, it only gets the data once as expected:
function getFoods() {
console.log("getFoods")
const dbRef = ref(getDatabase())
get(child(dbRef, (props.userId + "/" + props.date + "/" + props.meal))).then((snapshot) => {
if (snapshot.exists()) {
console.log(snapshot.val())
// setFoods(data)
} else {
console.log("No data available")
}
}).catch((error) => {
console.error(error)
})
}
How do I make it only run once when the component loads?
I am trying to upload an image to a cloud storage and received an post 400 error
The file is in initialize in another component.
Thanks for help
const projectStorage = getStorage();
useEffect(() => {
const storageRef = ref(projectStorage, file.name);
uploadBytes(storageRef, file).then((snap) => {
let percentage = (snap.bytesTransferred / snap.totalBytes) * 100;
setProgress(percentage);
console.log('File Uploaded');
}, (err) => {
setError(err);
}, async () => {
//this url doesnt change the setstate url becuase it is in seperate score
const url = await storageRef.getDownloadURL();
setUrl(url);
})
}, [file]);
I created a new project on firebase and then change the permissions on the storage rules and its works.
Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/
Please can someone help me. I think the way I am handling the promise is wrong and really need someone to help me.
I am letting the user upload a picture . When the user presses submit the image is uploaded to firebase-storage. However I don't think I am handling the wait period to upload the image and setting the data to firebase-database. What I mean is when I press submit I get the error FireBase Function DocumentReference.set() called with invalid data Because it is setting the image to undefined
However if I wait a couple of seconds I get the console.log("File available at" + downloadUrl) which means the image was uploaded.
Basically I just need to add a waiting period to my code between when the image is uploaded and when to send the data to the firebase-database
This is my code any help will be much appreciated !!!!!
const uploadImage = async (uri, imageName) => {
const response = await fetch(uri)
const blob = await response.blob()
var ref = firebase.storage().ref().child(`images/${imageName}`)
ref.put(blob)
.then(()=>{
// Upload completed successfully, now we can get the download URL
var storageRef = firebase.storage().ref('images/' + imageName)
storageRef.getDownloadURL().then((downloadUrl)=>{
console.log(`File available at ${downloadUrl}`)
setDownload(JSON.stringify(downloadUrl))
})
})
.catch(error => {
setRefreshing(false) // false isRefreshing flag for disable pull to refresh
Alert.alert("An error occured", "Please try again later")
});
}
const handleSubmit = useCallback(() => {
if (postImage !== undefined) {
const fileExtention = postImage[0].split('.').pop()
const fileName = `${uniqid}.${fileExtention}`
uploadImage(postImage, fileName)
firebase.firestore()
.collection('Posts')
.doc(uniqid)
.set({
id: currentUser,
name: postName[0],
image: downloadImage,
})
}
})
Thank you in advance for all your help!!!!!
To use await inside useCallback you can try to wrap the code inside it in a self invoking function like this:
const handleSubmit = useCallback(() => {
(async () =>{ if (postImage !== undefined) {
const fileExtention = postImage[0].split('.').pop()
const fileName = `${uniqid}.${fileExtention}`
uploadImage(postImage, fileName)
await firebase.firestore()
.collection('Posts')
.doc(uniqid)
.set({
id: currentUser,
name: postName[0],
image: downloadImage,
})
}
})()
})