Firestore onSnapshot appending to array through state within onEffect - javascript

I'm currently creating a to-do list within React which retrieves tasks from Firestore and stores them locally within an array using state hooks: const [todoList, setTodoList] = useState([]). I've run into some roadblocks while coding this mainly because Firestore's onSnapshot function doesn't seem to play properly with React. The snapshot code is called on load (to retrieve existing tasks) and when a new task is created. The snapshot code for appending changes to an array is:
todoReference.onSnapshot(colSnapshot => {
colSnapshot.docChanges().forEach(change => {
if (change.type === 'added') {
const taskData = change.doc.data();
todoList.push(taskData);
}
});
setTodoList(todoList); // "update state" using new array
});
There are a few issues which pop-up when I try different variations of this (pushing to empty array and then concatenating the two arrays together, etc.):
The todo list state doesn't persist on new snapshot. For example, creating a new task task2 updates the todoList to [task2], but creating another task task3 after that makes the first task disappear and updates the array to [task3] instead of [task2, task3].
onSnapshot keeps retrieving the same tasks despite them being previously retrieved. For example, on load the initial todoList is updated to [task1, task2, task3]. When creating a new task and calling the snapshot function again, I expect the todoList to be updated to [task1, task2, task3, task4]. However, instead I'm returned some variation of [task1, task2, task3, task1, task2, task3, task4] which compounds whenever the snapshot function is called again.
This issue seems to only happen in React and not native JavaScript (the tasks are created and retrieved just fine without any duplicates). Some solutions I have tried is wrapping everything within a onEffect (which I believe gave me the first problem if I didn't pass todoList as a dependency, and if I did would infinitely loop) and calling the snapshot function via unsubscribe() (which still gave me the second problem).

Solved! I nested everything within a useEffect with no dependencies and managed to resolve the first bullet-point regarding states not updating properly. Instead of setting state normally using setTodoList(todoList.concat(newTasks)), I functionally set the state using setTodoList(currentList => currentList.concat(newTasks)) (something about setState being asynchronous about useState, async isn't my specicalty). Find the answer here: https://stackoverflow.com/a/56655187/9477642.
Here's the final snapshot update code (I somehow also resolved onSnapshot returning the entire collection instead of just changes each time but I forgot how, I'll update this if I remember why):
useEffect(() => {
let unsubscribe = todoReference.onSnapshot(colSnapshot => {
console.log(colSnapshot.docChanges());
let newTasks = [];
colSnapshot.docChanges().forEach(change => {
if (change.type === 'added') {
const taskData = change.doc.data();
newTasks.push(taskData);
}
});
setTodoList(currentList => currentList.concat(newTasks));
});
return () => unsubscribe();
}, []);

Related

firebase onSnapshot how to refresh arrays to avoid duplicate keys?

here is the code im using to listen to changes in my firestore database:
async mounted() {
let id = [];
let orders = [];
await db.collection("orders").onSnapshot(doc => {
includeMetadataChanges: true;
doc.docs.forEach(x => {
id.push(x.id);
let z = Object.assign(x.data(), { id: x.id });
orders.push(z);
});
});
I'm using vuejs, adding this listener on the mount stage so the arrays depending on this snapshot keep refreshing. but I'm facing a problem which is when changes happen to the database my new snapshot adds the data to the array which results in duplicate keys all over, I can't find an efficient way to reset the arrays on each snapshot before inserting the new version.
id array is an array I use to extract the id then insert it inside the orders array so I can use it internally.
Edit:
async mounted() {
let id = [];
let orders = [];
await db.collection("orders").onSnapshot(doc => {
includeMetadataChanges: true;
orders = []
id = []
doc.docs.forEach(x => {
id.push(x.id);
let z = Object.assign(x.data(), { id: x.id });
orders.push(z);
});
});
console.log(orders)
when I reset orders array in callback I get an empty array.
Edit 2- i found the error:
I miscalculated where to place the save the array.
this.$store.dispatch("mystore/saveOrders", orders);
I should have placed it inside the onSnapshot function so each time it runs I do the save, at first I had in the mounted function right after the onsnapshot listener but I had to reset orders as mentioned in the answer by Frank van Puffelen.
You have two options here:
The simplest one is to clear the array every time your callback gets invoked, because doc.docs contains all relevant data to rebuild the UI anyway. So this would be calling orders = [] at the top of the callback.
If you want more granular control over updating the UI, you can iterate over doc.docChanges, which allows you to view the changes between the snapshots. So with this you can determine what documents were added to the snapshot (initially that'll be all of them), which ones were removed, and which docs were updated.
Many UI frameworks these days perform minimal updates to the UI based on the data you provide them with, so I'd definitely recommend checking whether that is the case for Vue too before taking the second approach.

How can I have react batch multiple setStates for fetched data from useEffect?

I am still trying to wrap my head around how react handles renders and this particular behavior has had me scratching my head all day. When i run this code, I get 3 console logs. The first is a null, as expected since useEffect didn't run yet. Next, I get the fetched worldData array from my api call as expected. However, I then get a third console log with the same said array, which leads me to believe my component is being rerendered. If I add another set state and another api call, I see yet another console log.
function App() {
const [worldData, setWorldData] = useState(null)
const [countriesData, setCountriesData] = useState(null)
useEffect(() => {
const fetchData = async () => {
const [world, countries] = await Promise.all([
fetch("https://disease.sh/v3/covid-19/countries?yesterday=true").then(res => res.json()),
fetch("https://disease.sh/v3/covid-19/all?yesterday=true").then(res => res.json())
])
.catch((err) => {
console.log(err)
})
setWorldData(world)
setCountriesData(countries)
}
fetchData();
}, []);
console.log(worldData);
It seems like react is rendering every time I set a State, which is what I assume it's designed to do. However, I've read elsewhere that react batches multiple set states together when set together in useEffect. So why is my set states not being batched then? Is it because of the asynchronous code? If so, is there a way I can fetch multiple endpoints and set all the retrieved data simultaneously so that my component only needs to rerender once?
A couple bits
(1) If you're ONLY concerned about having a single state update then you can defined a single state and update that as such
const [fetched_data, updateFetchedData] = useState({world:null, countries:null});
...
// where *world* and *countries* are variables containing your fetched data
updateFetchedData({world, countries})
...
fetched_data.world // using the fetched data somewhere
(2) HOWEVER, your code should not care about receiving multiple updates and should cater for it as such...
Your useState should define what an empty result would look like, for example instead of
const [worldData, setWorldData] = useState(null); // probably shouldn't look like this
how about
const [worldData, setWorldData] = useState([]); // this is what an empty result set looks like
And inside whatever component that uses both world and countries data ( I assume one references the other which is why you want both updates at once), just put a conditional statement that if it's not found then put some string 'Loading' or 'NA' or whatever until it's loaded.

Can't seem to affect state from within React's useEffect() with empty dependencies array. Socket.io

Basically I think my question is why is it that why can't i access allUserMessages from my state within useEffect with nothing in its dependency array? More details below. Thank you.
I am trying to push to an array in my state with useeffect like componentDidMount for incoming websockets data. But when messages come in it seems to push the most recent message but doesnt keep the rest of the array because in my chat log im only getting the most recent message sent instead of a log of all the messages. When I take out the empty dependencies array in my useeffect function, it can push to the array and has the full chat log, but it seems to receive the message recursively with each message in my array so by the 5th or 6th message the app becomes very slow It console logs the same message a bunch of times and increases with each message. Please have a look at my code if you wouldn't mind and let me know if you have any ideas. I really aprpreciate it.
const socket = io.connect()
const Community = () => {
const [userMessage, setUserMessage] = useState("");
const [allUserMessages, setUserMessages] = useState([])
const [showMentions, setShowMentions] = useState(false)
const [activeUsersForMentions, setActiveUsersForMentions] = useState([])
const [mentionIDs, setMentionIDs] = useState([])
const [userID, setUserID] = useState()
useEffect(()=>{
socket.on("incomingMessage", (data) => {
console.log(data);
socket.emit("joinRoom", {roomid: 'lounge'})
console.log('joining room');
//get the current list of all messages on the channel
//push the new message with the info from data
let isUserMentioned = false;
if(typeof data.mentions.find(x=>x === userID) != 'undefined'){
isUserMentioned = true;
}
console.log(allUserMessages);
//set state again with modified array to include the new message
setUserMessages([...allUserMessages, {first_name: data.first_name,
last_name: data.last_name,
avatar: data.avatar,
message: data.message,
account_type:data.account_type,
isMentioned: isUserMentioned}])
})
},[])
Because the dependencies array is empty, within your callback, you're guaranteed that allUserMessages will be [] (its initial state), because your callback closes over that initial state and is only called once, after the component is initially created. But you get messages from the socket repeatedly, even after updating your state.
To fix it, use the callback version of setUserMessages:¹
setUserMessages(allUserMessages => [...allUserMessages, {first_name: data.first_name,
// −−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^
last_name: data.last_name,
avatar: data.avatar,
message: data.message,
account_type:data.account_type,
isMentioned: isUserMentioned
}]);
That way, you'll always get the up-to-date version of allUserMessages, instead of only the version that was current as of when your component was first created.
In general, in React, if you're updating a state item based on an existing state item, 99% of the time you want to use the callback version of the state setter so that you always have the most up-to-date state.
¹ It really should be called setAllUserMessages (or the state item should be userMessages rather than allUserMessages) because the state item is allUserMessages. That's just convention, but it's the overwhelmingly common convention.
Right now, because you have an empty dependencies array, your useEffect hook is only running once when you load the component. If you remove the dependecies array altogether, your useEffect will keep running indefinitely. You need to set a proper dependency in the array that will make your useEffect hook run everytime that value changes.

Why is my state not updating with fetched data in time for the useEffect to update my DOM with the new state?

I am new to using hooks in React. I am trying to fetch data when the component first mounts by utilizing useEffect() with a second parameter of an empty array. I am then trying to set my state with the new data. This seems like a very straightforward use case, but I must be doing something wrong because the DOM is not updating with the new state.
const [tableData, setTableData] = useState([]);
useEffect(() => {
const setTableDataToState = () => {
fetchTableData()
.then(collection => {
console.log('collection', collection) //this logs the data correctly
setTableData(collection);
})
.catch(err => console.error(err));
};
setTableDataToState();
}, []);
When I put a long enough timeout around the setTableData() call (5ms didn't work, 5s did), the accurate tableData will display as expected, which made me think it may be an issue with my fetch function returning before the collection is actually ready. But the console.log() before setTableData() is outputting the correct information-- and I'm not sure how it could do this if the data wasn't available by that point in the code.
I'm hoping this is something very simple I'm missing. Any ideas?
The second argument passed to useEffect can be used to skip an effect.
Documentation: https://reactjs.org/docs/hooks-effect.html
They go on to explain in their example that they are using count as the second argument:
"If the count is 5, and then our component re-renders with count still
equal to 5, React will compare [5] from the previous render and [5]
from the next render. Because all items in the array are the same (5
=== 5), React would skip the effect. That’s our optimization."
So you would like it to re-render but only to the point that the data changes and then skip the re-render.

Component not accessing context

i'm new to react please forgive me if i'm asking a dumb question.
The idea is to access the tweets array from context, find the matching tweet and then set it in the component's state to access the data.
However, the tweets array results empty even though i'm sure it's populated with tweets
const { tweets } = useContext(TweeetterContext)
const [tweet, setTweet] = useState({})
useEffect(() => {
loadData(match.params.id, tweets)
}, [])
const loadData = (id, tweets) => {
return tweets.filter(tweet => tweet.id == id)
}
return (stuff)
}
You are accessing context perfectly fine, and it would be good if you could share a code where you set tweets.
Independent of that, potential problem I might spot here is related to the useEffect function. You are using variables from external context (match.params.id and tweets), but you are not setting them as dependencies. Because of that your useEffect would be run only once at the initial creation of component.
The actual problem might be that tweets are set after this initial creation (there is some delay for setting correct value to the tweets, for example because of the network request).
Try using it like this, and see if it fixes the issue:
useEffect(() => {
loadData(match.params.id, tweets)
}, [match.params.id, tweets])
Also, not sure what your useEffect is actually doing, as it's not assigning the result anywhere, but I'm going to assume it's just removed for code snippet clarity.

Categories