How do I properly call a function in a useEffect? Only want to call the function on page load.
I've got multiple functions that are being called in a use effect but the way it's set up I'm getting 1000+ reads and maxing out my quota instantly.
const [user, loading] = useAuthState(auth);
const [myDisplayName, setMyDisplayName] = useState("");
const [projectList, setProjectList] = useState([])
useEffect(()=>{
if(!user) navigate("/login")
fetchUserName();
fetchTotalProjects();
},[])
const fetchUserName = async () => {
try {
const query = await db
.collection("user")
.where("uid", "==", user?.uid)
.get();
const data = await query.docs[0].data();
setMyDisplayName(data.firstName);
} catch (err) {
console.error(err);
}
};
const fetchTotalProjects = async () => {
try {
const query = await db
.collection("projects")
.where("uid", "==", user?.uid)
.get()
.then((snapshot) => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setProjectList(tempData);
});
const data = await query.docs[0].data();
setProjectList(data);
} catch (err) {
console.error(err);
}
};
Fixed the issue. Getting the UID from Firebase was coming back undefined the first 3 -5 times and then it would get it, meaning the useEffect would fire until the data was retrieved.
Solution: Get your user uid from Firebase like this
const user = auth?.currentUser;
Related
I have this function which is setting the user
const [user, setUser] = useState(null)
const getUser = async () => {
setIsAuth(false)
setLoading(true)
try {
let temp = await axiosURL.get('/api/users/me')
temp = temp.data
if (temp) {
setUser(temp)
setIsAuth(true)
}
} catch (error) {
console.log(error)
}
setLoading(false)
}
After setting the user I am fecthing the data acording to the user
const getAllScopeData = async () => {
setLoading(true)
await getUser()
console.log(user)
await getScopeOneData()
await getScopeTwoData()
await getScopeThreeData()
setLoading(false)
}
But when I am running getAllScopeData() The user details is fetching correctly and logging in the console but user from the state is not changing from null . So I am able to fetch further data .
the problem I see it's here
const getAllScopeData = async () => {
setLoading(true)
await getUser()
console.log(user)
await getScopeOneData()
await getScopeTwoData()
await getScopeThreeData()
setLoading(false)
}
even if you wait for the function getUser your state is not update yet
You should use useEffect like this
useEffect(async() => {
if(!user) return;
setLoading(true)
await getScopeOneData()
await getScopeTwoData()
await getScopeThreeData()
setLoading(false)
}, [user])
I have a simple function that checks if the user has Premium access or not:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
if (doc.id === auth.currentUser.uid) {
return doc.data().userSettings.hasPremium
}
});
})
}
else{
return false
}
}
I tried to catch this in various ways, but no luck, it always returns an "undefined" object.
const getPremium = async => {
checkPremium.then((response) => console.log(response))
}
const getPremium = async => {
let hasPremium = await checkPremium()
}
let hasPremium = checkPremium()
What is the correct way to get the returned Boolean value?
onSnapshot is meant for listening to a collection continuously, getting repeatedly notified as its value changes. It does not create a promise, so the promise returned by getPremium is unrelated to the data you will eventually get in onSnapshot. If you just want to get the value once, you should use getDocs:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
const querySnapshot = await getDocs(q);
const match = querySnapshot.docs.find(doc => doc.id === auth.currentUser.uid);
if (match) {
return doc.data().userSettings.hasPremium);
} else {
return false;
}
}
else{
return false
}
}
Also, instead of getting all the users and then using client side code to find the one with the right id, you could just fetch that individual doc directly:
const ref = doc(db_firestore, 'users', auth.currentUser.uid)
const snapshot = await getDoc(ref);
const data = snapshot.data();
if (data) {
return data.userSettings.hasPremium
} else {
return false
}
i have a onSnapshot query in a function:
//firebaseutil.js
export async function getShorts(uid) {
const q = query(collection(db, 'shorted'), where('owner', '==', uid));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const urls = [];
querySnapshot.forEach((doc) => {
urls.push({
url: doc.data().url,
shorturl: doc.data().shorturl,
hits: doc.data().hits,
});
});
console.log(urls);
return urls;
});
}
Which correctly logs the data, and relog it if i change it on the firestore collection (as expected)
i am trying to access these data from a user dashboard this way:
//dashboard.js
import { getShorts } from '../lib/fbutils';
import { useEffect, useState } from 'react';
export default function Dashboard() {
const [myurls, setUrls] = useState([]);
useEffect(() => {
const fetchShorts = async () => {
if (user) {
const urls = await getShorts(user.uid);
setUrls(urls);
console.log(myurls);
console.log(urls);
}
};
fetchShorts();
}, []);
user.id is correctly set, but both urls and myurls are logged as undefined (i was thinking at least for a Promise pending)
what am i doing wrong? i usually use this pattern to retrieve data, but it's my first time i get data from a firestore subscription
The onSnapshot() does not return a promise and your getShorts() function returns before the data is received. You can return a promise from that function as shown below:
let fetched = false;
export function getShorts(uid) {
return new Promise((resolve, reject) => {
const q = query(collection(db, 'shorted'), where('owner', '==', uid));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const urls = querySnapstho.docs.map((d) => ({ id: d.id, ...d.data() }))
if (!fetched) {
// URLs fetched for first time, return value
fetched = true;
resolve(urls);
} else {
// URLs fetched already, an update received.
// TODO: Update in state directly
}
})
})
}
This should return all the URLs when you call the function await getShorts(user.uid); but for the updates received later, you'll have to update them in the state directly because the promise has resolved now.
New to React Hooks and unsure how to solve. I have the following snippet of code within my App.js file below.
What I am basically trying to achieve is to get the user logged in by calling the getUser() function and once I have the user id, then check if they are an authorised user by calling the function checkUserAccess() for user id.
Based on results within the the validIds array, I check to see if it's true or false and set authorised state to true or false via the setAuthorised() call.
My problem is, I need this to process first prior to performing my first render within my App.js file.
At the moment, it's saying that I'm not authroised even though I am.
Can anyone pls assist with what I am doing wrong as I need to ensure that authorised useState is set correctly prior to first component render of application, i.e. path="/"
const [theId, setTheId] = useState('');
const [authorised, setAuthorised] = useState(false);
const checkUserAccess = async (empid) => {
try {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
if (isAuthorised) {
setAuthorised(true)
} else {
setAuthorised(false)
}
} catch (err) {
console.error(err.message);
}
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
setTheId(theId);
checkUserAccess(theId);
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
Unless you are wanting to partially render when you get the user ID, and then get the access level. There is no reason to have multiple useState's / useEffect's.
Just get your user and then get your access level and use that.
Below is an example.
const [user, setUser] = useState(null);
const checkUserAccess = async (empid) => {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
return isAuthorised;
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
const access = await checkUserAccess(theId);
setUser({
theId,
access
});
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!user) return <div>Loading</div>;
return <>{user.theId}</>
This way it should work
but keep in mind that you must render your app only if theId in the state is present, which will mean your user is properly fetched.
const [state, setState] = useState({ theId: '', isAutorized: false })
const getUser = async () => {
try {
const idResp = await fetch("http://localhost:4200/get-user");
const theId = await idResp.json();
const authResp = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await authResp.response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(theId);
setState({ theId, isAuthorised })
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!state.theId) return <div>Loading</div>;
if (state.theId && !isAuthorized) return <AccessNotAllowed />
return <Home />
I'm using firebase - firestore. I have courses and tasks collection.
I want to get all the courses of user from courses collection and for each course to get days data from tasks collection and then save all this data in one array.
getData = () => {
var arr = []
f.auth().onAuthStateChanged(async (user) => {
db.collection("courses")
.where("uid", "==", user.uid)
.get()
.then((snapshot) => {
var a = {};
snapshot.forEach((doc) => {
let coursesData = doc.data()
let courseName = coursesData.name;
let kita = coursesData.kita;
a = { name: courseName, id: doc.data().code, k: kita };
let snapshotData = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get();
let numActiveCourse = 0;
snapshotData.forEach((dc) => {
let taskData = dc.data()
console.log('taskData',taskData)
let days = taskData.days;
if (days > 0) {
numActiveCourse = 1;
}
});
a = { ...a, numActiveCourse };
arr.push(a);
console.log("arr2 is", arr);
});
})
.catch((e) => {
console.log("error is courses", e);
});
this.setState({data:arr})
});
};
the problem is that the arr is always empty (I guess I have asyncornize issue)
and the snapshot not await after it will finish.
I found solution.
the issue it's because I tried to make async await into forEach and it not wait to answer.
the solution is
readCourses = async()=>{
f.auth().onAuthStateChanged(async (user) => {
let loadedPosts = {};
let docSnaps = await db.collection("courses").where("uid", "==", user.uid).get();
for (let doc of docSnaps.docs){
let courseName = doc.data().name;
let kita = doc.data().kita
loadedPosts[doc.id] = {
...doc.data(),
k:kita,
id:doc.data().code
}
const taskSnap = await db
.collection("tasks")
.where("uid", "==", user.uid)
.where("name", "==", courseName)
.where("kita", "==", kita)
.get()
let numActiveCourse = 0
for(let task of taskSnap.docs){
let taskData = task.data()
if(taskData.days>0){
numActiveCourse =numActiveCourse+1
}
}
loadedPosts[doc.id].numActiveCourse = numActiveCourse
}
console.log('loadedPosts',loadedPosts)
this.setState({data:loadedPosts})
})
}
if you have any other solution I would like to see.
It's not a good idea to mix await and then as it was in your original code. There was your first mistake. Not only you didn't wait for results of forEach, but this.setState({data:arr}) was outside of then and executed even before you've reached the forEach call.
Another issue with your initial version of code is as you said - not waiting for results of forEach. But I'm not sure that you fully understand it. Because you didn't have to change your code so much (readability aside). All your had to do is:
// change db.collection("courses")...then(...) to
const snapshot = await db.collection("courses")... // only now onAuthStateChanged callback becomes async
...
// then change forEach() to map() and wait for result
const promises = snapshot.map(async (doc) => { ... })
await Promise.all(promises)
...
As for your new version: in each iteration of the for loop you await. That means that requests for every taskSnap will be executed one after another. That's bad. Especially on slow connections. Check out the snippet (I've simplified it to the bare minimum): getData with map completes in 1 second, version with for - in 4 seconds. (And you also removed catch from your new code - not a great idea.)
let i = 0
const get_courses = () => new Promise((resolve) => setTimeout(() => resolve(["a","b","c","d"]), 10))
const get_tasks = () => new Promise((resolve) => setTimeout(() => resolve(++i), 1000))
const f_auth_onAuthStateChanged = fn => fn()
const getData = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
const promises = courses.map(async (course) => {
const tasks = await get_tasks()
data.push({ course, tasks })
})
await Promise.all(promises)
console.log(data) // this.setState({ data })
console.timeEnd("map")
} catch(e) { console.error(e) }
})
}
console.time("map")
getData()
const getData2 = () => {
const data = []
f_auth_onAuthStateChanged(async (user) => {
try {
const courses = await get_courses()
for (const course of courses) {
const tasks = await get_tasks()
data.push({ course, tasks })
}
console.log(data) // this.setState({ data })
console.timeEnd("for")
} catch(e) { console.error(e) }
})
}
console.time("for")
getData2()
The readCourses function from your own answer doesn't return a Promise. So formally it's not async. That won't change anything, except for a small code readability improvement. Same goes for onAuthStateChanged callback from your original code.