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?
Related
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.
technologies used: React Native / Expo / Firebase
I explain to you, I recover a list of key with my first onValue, in this one I forEach for each key and in this one I make another onValue in order this time to recover the information of the keys, but I realize that it creates a problem, one event calls another and it creates duplicate calls. Moreover I could not intervene on the second listening to delete it since it is only initialized locally, so how can I call this in the right way? thank you in advance for your answers, I'm new to react native with firebase!
Here is a snippet of the code:
useFocusEffect( // first time the screen is loaded, the gardener list is empty, so we need to get the gardener list from firebase
React.useCallback(() => {
setGardeners([])
const gardenerRef = ref(db, 'users/' + auth.currentUser.uid + '/gardeners');
const gardenerListener = onValue(gardenerRef, (snapshot) => {
const data = snapshot.val();
if (data != null) {
setIsFirst(false)
Object.keys(data).forEach(e => {
const temp = ref(db, 'gardeners/' + e);
onValue(temp, (snapshot) => {
const data = snapshot.val();
if (data != null && data.metadata != undefined) {
setGardeners(gardeners => [...gardeners, { id: e, name: data.metadata.name, isIrrig: data.irrig }]);
}
})
gardeners.length > 0 ? setCurrentGardener(gardeners[0].id) : setCurrentGardener("")
setC(currentGardener != "" ? currentGardener : "")
})
} else {
setIsFirst(true)
}
})
and here is the way i turn off the event during disassembly, for me it looks correct, now if you think i did it wrong, don't hesitate
return () => {
setGardeners([])
off(gardenerRef, gardenerListener)
// console.log("unmounted")
}
}, []))
I'm trying to push a new screen to display the selected item's detail, when I get the data it fetches the document correctly, however if I try to set my object to use it in my screen it is undefined, but if I reload the app it does populate the object, here's the code:
const [event, setEvent] = useState();
const getEvent = async (dbx) => {
const eventDoc = doc(dbx, 'events', eventId);
try {
const eventSnapshot = await getDoc(eventDoc);
if(eventSnapshot.exists()) {
const fetchedEvent = eventSnapshot.data();
setEvent(fetchedEvent);
} else {
console.log("Document does not exist")
}
} catch(error) {
console.log(error)
}
return;
}
useEffect(
()=>{
getEvent(db)
console.log("Event Here: ", event);
}
,[])
Setting a variable in the state is an asynchronous operation. In fact, so is loading data from Firestore, so neither of those operations is done by the time your console.log("Event Here: ", event) runs.
If you want to log the event value, use another effect that depends on that state variable to do so:
useEffect(()=>{
console.log("Event Here: ", event);
},[event])
I am succesfully updating my user's profile picture on their profile and on all of their reviews posted with this function:
export const storeUserProfileImage = (url) => {
const { currentUser } = firebase.auth();
firebase.database().ref(`/users/${currentUser.uid}/profilePic`)
.update({ url });
firebase.database().ref('reviews')
.orderByChild('username')
.equalTo('User3')
.once('value', (snapshot) => {
snapshot.forEach((child) => {
child.ref.update({ profilePic: url });
});
});
};
I am aware that I should be using an atomic update to do this so the data updates at the same time (in case a user leaves the app or something else goes wrong). I am confused on how I can accomplish this when querying over child values.
Any help or guidance would be greatly appreciated!
Declare a variable to store all the updates. Add the updates as you read them on your listener's loop. When the loop is finished, run the atomic update.
export const storeUserProfileImage = (url) => {
const { currentUser } = firebase.auth();
firebase.database().ref('reviews')
.orderByChild('username')
.equalTo('User3')
.once('value', (snapshot) => {
var updates = {};
updates[`/users/${currentUser.uid}/profilePic`] = url;
snapshot.forEach((child) => {
updates[`/reviews/${child.key}/profilePic`] = url;
});
firebase.database().ref().update(updates);
});
};
I was able to achieve the following -> When a user clicks on a particular date in component A the data gets sent to the Node (Sails API) where all the necessary calculations are done, and before component B is rendered the correct data is ready to be shown.
The problem is when a user returns back from component B to component A and chooses a different date, he/ she gets the exact same result (old value) because even though the new value is sent to the backend API, Node isn't doing the recalculations with the new value.
I'm only able to achieve the correct result after I manually refresh the page, or make changes to the server so it forces the recalculation.
I think I need to mention that I'm passing data using Redux, so maybe the issue occurs on that part.
I would consider some type of auto refresh, animated loading, anything.
Yup, so stuck :/
Is it even possible to make them in total sync?
UPDATE --> Here is the code:
BACKEND
getDetails: (req, res) => {
authentication.authenticate().then((auth) => {
const sheets = google.sheets('v4');
sheets.spreadsheets.values.get({
auth: auth,
spreadsheetId: config.spreadsheetSettings.spreadsheetId, // id of spreadsheet
range: config.spreadsheetSettings.employeeSheetId, // name of employee spreadsheet and range- get all cells
}, (err, response) => {
if (err) {
res.serverError(err);
return;
}
const rows = response.values; // response-all cells
const updatedData = employeeService.mapEmployeeSheetToJson(rows);
// FETCHING THE VALUE FROM REST API
let myArr = [];
(function() {
axios.get(`http://localhost:1337/api/`)
.then(res => {
let kajmak = res.data.slice(-1)[0]
let test = kajmak[Object.keys(kajmak)[0]]
myArr.push(test)
}).catch(err => console.error(err));
})();
// MAPING OVER THE ARRY AND DOING THE LOGIC
setTimeout(() => {
myArr.map(xo => {
const result = [];
updatedData.forEach(emp => {// 2013 2012 2014
if (xo > parseInt(moment(emp.startdate).format('YYYYMM'), 10) &&
(xo < parseInt(moment(emp.enddate).format('YYYYMM'), 10))) {
result.push(emp);
}
});
// IF THEY STARTED WORKING BEFORE THE SELECTED DATE AND STILL WORKING
updatedData.forEach(emp => { // 2013 > 2012 & 2013 -
if (xo > parseInt(moment(emp.startdate).format('YYYYMM'), 10) &&
((parseInt(moment(emp.enddate).format('YYYYMM'), 10) == undefined ))) {
result.push(emp);
}
});
// IF THEY STARTED WORKIG BEFORE THE SELECTED DATE,
// BUT STOPPED WORKING BEFORE THE SELECTED DATE
updatedData.forEach(emp => { // 2013 < 2014 || 2013 > 2017
if (xo < parseInt(moment(emp.startdate).format('YYYYMM'), 10) &&
(xo > parseInt(moment(emp.startdate).format('YYYYMM'), 10))) {
result.pop(emp);
}
});
// Getting the names to use for unique sheet req
let finalResult = [];
result.map(x => {
finalResult.push((x.name + ' ' + x.surname))
})
if (rows.length === 0) {
res.err('No data found.');
} else {
res.ok(finalResult);
}
})
}, 1000);
});
}
FRONTEND
getEmployeeSalaryData = () => {
// GETTING THE CLICKED VALUE FROM THE PREVIOUS COMPONENT
const { year } = this.props.history.location.state.item;
const { month } = this.props.history.location.state.item;
const selectedMonth = moment().month(month).format("MM");
const finalSelect = parseInt(year + selectedMonth, 10);
const { employees } = this.props;
// I'M RECIEVING THIS AS PROPS USING REDUX AND THIS IS THE ACTUAL 'FINAL' DATA USED FOR FURTHER CALCS AND RENDERING
const { details } = this.props;
// HERE I'M SENDING THE 'CLICKED' VALUE FROM THE PREVIOUS COMPONENT TO THE BACKEND API
axios.post(`http://localhost:1337/api/`, { 'test' : finalSelect })
.then(res => {
console.log('Data send')
// console.log(res.data);
}).catch(err => console.error(err));
// Making the req
details.map(x => {
EmployeeApi.getEmployee(x)
.then(y => {
//Making sure everything is in the right order
let test = Object.assign(y.data);
let ii = x;
setTimeout(
this.setState(prevState => ({
...prevState.currentEmployee,
fullNames: [...prevState.currentEmployee.fullNames, ii]
})), 100);
let onlyRelevantDate = [];
test.map(item => {
if (finalSelect == parseInt(item.year + moment().month(item.month).format("MM"), 10)) {
onlyRelevantDate.push(item)
}})
this.setState(prevState => ({
currentEmployee: {
...prevState.currentEmployee,
salaryInfo: [...prevState.currentEmployee.salaryInfo, onlyRelevantDate],
fullNames: [...prevState.currentEmployee.fullNames, ii]
}}))
})
});
}
componentWillReceiveProps(nextProps) {
this.getEmployeeSalaryData(nextProps);
}
componentWillMount() {
this.getEmployeeSalaryData(this.props);
}
In component A you should dispatch an action that is a function taking a dispatch function.
//some click handler for when user makes a selection
// the function should be in action creator file but you get the jist
const handleSomeClick = someValue =>
//when you dispatch an action that is a function in redux with thunk then
// the thunk middleware will not call next (no reducers will be called)
// thunk will pass a parameter to this function that is the dispatch
// function so from your function you can dispatch actual object action(s)
dispatch(
dispatch=>
setTimeout(
dispatch({type:"changedValue",data:someValue}),//dispatching the action
someValue*1000//assuming someValue is a number
)
)
Here is an example that has component A set someValue depending on what button is clicked and will highlight that button it'll also set someValue of B asynchronously. This is done in the function changeLater that dispatches an action that is a function so thunk will execute it with the dispatch.
This function will dispatch an action after a timeout. If you click the numbers 5 and then 1 (quickly) you'll see that the highlighted button of A and value after async of B do not match (highlighted of A is 1 and value after async of B is showing 5).
This is because the order of which the user clicks and starts the async process is not the same as the order the async process resolves. You could solve this by only dispatching an action when it's the last resolved promise.
This example shows how it's done by using a promise created by later and only resolve it if it's the last by using a partially applied version of onlyLastRequestedPromise called lastNumberClicked
you can use RxJS to solve this