State not updating until external event - reactjs

I am using useEffect to run a function that grabs data from firebase.
The data is grabbed fine, however the setState function does not seem to take effect until after another state has changed.... I thought using useEffect would run before first render.
function App() {
const [expenses, setExpenses] = useState([]);
const roster = [];
React.useEffect(() => {
const getRoster = async () => {
var db = firebase.firestore();
db.collection("Roster")
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
var data = doc.data();
data.ID = doc.id;
roster.push(data);
});
});
console.log("setting roster");
setExpenses(roster);
console.log("roster", roster);
console.log("expenses", expenses);
};
getRoster();
}, []);
the console returns the following
setting roster
roster [all data from firebase here]
expenses [blank], **expenses is the state variable**
The expenses state only updates after I change some other state in the application. I've tried to work around this by changing some other states in the use effect function, no dice. Also I've tried passing the state as a dependency to the use effect. Nothing...
I must doing something wrong but I'm not sure what that is.
My goal is to have the expenses state updated on first page load.

setExpenses(roster); should be called inside .then as it .get is an async call and it takes some time so within this time your setExpenses(roster); gets called and its has the initial value of roster. So you should use your setExpenses(roster); as bellow
React.useEffect(() => {
const getRoster = async () => {
var db = firebase.firestore();
db.collection("Roster")
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
var data = doc.data();
data.ID = doc.id;
roster.push(data);
});
console.log("setting roster");
setExpenses(roster);
console.log("roster", roster);
console.log("expenses", expenses);
});
};
getRoster();
}, []);

The setExpenses call should be placed directly after the querySnapshot.forEach call, but still within the .then((querySnapshot) => { ... } handler. Because it's currently placed after that handler, it is executed immediately on first render, not when the Firebase data is obtained.

Related

Firestore Async function

So I'm trying to use React Query with Firestore and usually this works pretty fine but recently I have been unable to make a function that returns a value.
const fetchProduct = async () => {
const querySnapshot = await getDocs(collection(db, "notes"));
const arr = [];
querySnapshot.forEach((doc) => {
setNotes(doc.data())
}).catch((error) => {
console.log(error);
return null;
});
return notes
}
I'm later trying to use this function with React Query like this -
const { isLoading, isError, data, error } = useQuery(['todos'], fetchProduct)
However the value of the {data} always return to undefined but however once the fetchProduct() function is called manually it all works.
Is there anything I'm missing or doing wrong?
Setting the state is an asynchronous operation in React (see docs), so the value of notes isn't set yet by the time your return notes runs.
If you want to return the value synchronously:
return querySnapshot.docs.map((doc) => doc.data());
More typically though, you'll want to put the code that depends on that return value into its own useEffect hook that depends on the notes state.
Also see:
The useState set method is not reflecting a change immediately
Why does calling react setState method not mutate the state immediately?
Is useState synchronous?
It should be like this, you should not set inside the foreach function
const fetchProduct = async () => {
const querySnapshot = await getDocs(collection(db, "notes"));
const notes = [];
querySnapshot.forEach((note) => {
notes.push({ ...note.data(), id: note.id })
}).catch((error) => {
console.log(error);
return null;
});
return notes;
}
// in the place of the calling the function
const notes = await fetchProduct();
setNotes(notes);
Note: don't use variable name as doc in foreach, instead use some other variable name like since doc is built in function of firebase you might have imported it

Why my useEffect that tries to get blockchain data is looping infinitely and my async func still returns Promise pending

I am trying to use async await inside a useEffect hook getting some data from a testnet blockchain but I am getting 2 problems:
The async function returns a Promise, why is that? Shouldn't async await automatically resolve the promise and give me the data? I tried to solve it with Promise.resolve but not working, it still tells me campaigns is a Promise in pending state.
It enters in an infinite loop and I still do not get why.
Here is the code:
useEffect(() => {
const getCampaigns = async() => {
const campaigns = await factory.methods.getDeployedCampaigns().call()
return campaigns
}
const campaigns = getCampaigns();
setCampaigns(Promise.resolve(campaigns));
console.log('campaigns: ', campaigns);
})
You have no dependencies array.
useEffect(() => {
const getCampaigns = async() => {
const campaigns = await factory.methods.getDeployedCampaigns().call()
return campaigns
}
const campaigns = getCampaigns();
setCampaigns(Promise.resolve(campaigns));
console.log('campaigns: ', campaigns);
}, [])
Try this
useEffect(() => {
const getCampaigns = async() => {
const campaigns = await factory.methods.getDeployedCampaigns().call()
setCampaigns(campaigns);
}
getCampaigns();
}, []);
The empty array in useEffect call makes it behave like component did mount and only executes once (assuming factory methods are initialized on mount) and since the getDeployedCompanigns Promise is already resolved I'm simply setting the state in the getCampaigns function.
Read this article for details: https://devtrium.com/posts/async-functions-useeffect

Updating the state correctly after fetch data from firestore

I am trying to use Firestore Snapchats to get real time changes on a database. I am using react-native-cli: 2.0.1 and react-native: 0.64.1 .
export const WelcomeScreen = observer(function WelcomeScreen() {
const [listData, setListData] = useState([]);
const onResult = (querySnapshot) => {
const items = []
firestore()
.collection("Test")
.get()
.then((querySnapshot) => {
querySnapshot.forEach(function(doc) {
const tempData = {key: doc.id, data: doc.data}
items.push(tempData);
});
setListData(items)
});
}
const onError = (error) => {
console.error(error);
}
firestore().collection('Test').onSnapshot(onResult, onError);
}
Every thing is working perfectly, until I use setListData to update the data list. The App does not respond anymore and I get a warning message each time I try to add or delete data from the database
Please report: Excessive number of pending callbacks: 501. Some pending callbacks that might have leaked by never being called from native code
I am creating a deadlock by setting the state this way?
First, you don't want to set up a snapshot listener in the body of your component. This results in a growing number of listeners, because every time you render you add a new listener, but every listener results in rendering again, etc. So set up the listener just once in a useEffect:
const [listData, setListData] = useState([]);
useEffect(() => {
function onResult(querySnapshot) {
// ...
}
function onError(error) {
console.log(error);
}
const unsubscribe = firestore().collection('Test').onSnapshot(onResult, onError);
return unsubscribe;
}, []);
In addition, your onResult function is going to get called when you get the result, and yet you're having it turn around and immediately doing a get to re-request the data it already has. Instead, just use the snapshot you're given:
function onResult(querySnapshot) {
const items = []
querySnapshot.forEach(function(doc) {
const tempData = {key: doc.id, data: doc.data()}
items.push(tempData);
});
setListData(items);
}

Only first element is showing state updates until external event

Related and an almost direct extension of my earlier question
I am using useEffect to run a function that grabs data from firebase.
The data is grabbed, however only the first element appears to show the additional information added to the state.
const [expenses, setExpenses] = useState([]);
const roster = [];
var db = firebase.firestore();
React.useEffect(() => {
const getRoster = async () => {
db.collection("Roster")
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
var data = doc.data();
data.ID = doc.id;
roster.push(data);
});
// setExpenses(roster);
// console.log("expenses", expenses);
// console.log("roster", roster);
})
This is working as expected, the commented out code was the solution to my previous question.
I added in the code below and experienced my new issue.
.then(() => {
roster.forEach((member) => {
let total = 0;
db.collection("Attendance Entries")
.where("attendees", "array-contains", member.ID)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
total++;
member.total = total;
});
setExpenses(roster);
});
});
});
};
getRoster();
}, []);
The first element shows the "member.total" on the inital load, the others only appear after the state is changed. Not sure why that is the case..
Thank you for the help!
Example of what I am seeing on initial load
I don't quite follow all of your code, but I see a possible issue with the way you enqueue state updates in the forEach loop in the section of code you say you've an issue with. When you enqueue state updates in a loop you must use a functional state update so each enqueued update doesn't blow away (overwrite) the previous state.
You can iterate the roster array you've previously computed and then compute the docs total for each member and mutate the member object, then use a functional state update to append the new member object to the expenses state array.
.then(() => {
roster.forEach((member, index) => {
let total = 0;
db.collection("Attendance Entries")
.where("attendees", "array-contains", member.ID)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
total++;
});
member.total = total;
setExpenses(expenses => expenses.concat(member));
});
});
});

Why do I need to put the function inside a setState method for it to work?

When a socket emits an event from the server side the App reloads for some reason and posts is emptied out. But when I define the function inside the setPosts it works perfectly. Why is this?
const App = () => {
let [user, setUser] = useState(null)
let [posts, setPosts] = useState({})
console.log('app')
useEffect(() => {
console.log('use effect')
socket.on('post', (post) => {
// THIS DOES NOT WORK:
// let newPosts = { ...posts }
// newPosts[post._id] = post
// setPosts(newPosts)
//THIS WORKS
setPosts((posts) => {
let newPosts = { ...posts }
newPosts[post._id] = post
return newPosts
})
})
async function getUser() {
let user = await actions.getUser()
if (user) {
setUser(user?.data)
}
}
getUser()
actions
.getAllPosts()
.then((res) => {
console.log('WE GOT ALL POSTSTFOM API', res.data)
const postsById = {}
for (let post of res.data) {
postsById[post._id] = post
}
console.log('wired')
setPosts(postsById)
//filterPosts(res.data)
})
.catch((err) => console.error(err))
return () => {
socket.off('post')
}
}, [])
This is how enclosures work in javascript. When you use a non-functional state update you are referencing the posts state value ({}) from the render cycle the callback was instantiated in, i.e. the initial render when the effect callback ran when mounted (note the empty dependency array). It's a stale enclosure of the posts state value.
When using a functional state update you are accessing and updating from the previous state, not the state from the previous render cycle (or enclosure).

Resources