react useEffect strange behaviour - can't explain? - reactjs

I have the following code in a useEffect
useEffect(() => {
async function fetchMessages() {
let messages = [];
const firestore = firebase.firestore();
const query = firestore.collection('chats').where("repliedTo", "==", false).where("type", "==", "StudentQuery").orderBy("timestamp", "desc");
query.onSnapshot({
next: (querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log("x ", doc.id, '=>', doc.data());
messages.push({mid: doc.id, ...doc.data()});
console.log(messages)
});
},
});
setMessagesList(messages)
setMessageCount(messagesList.length)
console.log('xxxx' + messagesList.length)
}
fetchMessages();
}, [messagesList.length]);
A few things seem to be wrong with this and I can't see it.
When I trigger this code (by inserting a new record into Firestore) what I expect would be to see a console.log with the (final) array size (so previous array size + 1) - but instead what I am seeing is the previous array + (previous array + 1 entry) I would have thought he let messages = [] would have cleared the array every time an update happened?
I never see the console.log("xxx") in my console. I want to put a state update here as this line should be safe as the database read has done, but since the line doesn't appear I don't know what's going wrong.
Can anyone shed some insight?

I've not used firebase before but it looks like you're effectively creating a subscription which is getting called outside of React's render cycle.
You could just add a state property that you update when next is called, eg:
const [messages, setMessages] = useState([]);
// Use the `useEffect` to set up / tear down the subscription
useEffect(() => {
const firestore = firebase.firestore();
const query = firestore.collection(...);
const unsubscribe = query.onSnapshot({
next: (querySnapshot) => {
setMessages(prev => [
...prev,
...querySnapshot.map(doc => ({
mid: doc.id,
...doc.data(),
})),
]);
});
});
// Unsubscribe when you unmount
return () => unsubscribe();
}, [])

Related

React - Listening for changes, then updating data from firestore

I have a calendar with multiple users, i need to listen for changes and refetch data when someone updates an event.
So, my problem is that when i fetch the events, the listener changes and it ends in a infinite loop for all users.
I dont need to do anything with the docs, i just need to know when one changes, then run my fetch function and rerender the events.
(or if anyone has a good alternative solution i'm all ears!)
const q = query(collection(database, eventDatabase));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const events = [];
querySnapshot.forEach((doc) => {
events.push(doc.data());
});
console.log("Event updated: ", events);
fetchEvents(eventDatabase)
});
My fetch events:
const fetchEvents = async (eventDatabase) => {
const querySnapshot = await getDocs(collection(database, eventDatabase));
let allEvents = []
querySnapshot.forEach((doc) => {
allEvents.push({
...doc.data(),
start: doc.data().start.toDate(),
end: doc.data().end.toDate()
})
});
return allEvents
}
Any tips greatly appreciated..

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);
}

State not updating until external event

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.

UseEffect infinite loop with Firebase

Hi i'm trying to build a simple chat, but I encountered some issues when reading the messages.
const [messages, setMessages] = useState([]);
const messagesArray = [];
const messagesRef = firestore().collection('messages');
useEffect(() => {
id
? messagesRef
.where('idRequest', '==', id)
.orderBy('createdAt')
.limit(25)
.get()
.then((response) => {
response.forEach((doc) => {
messagesArray.push(doc.data());
});
setMessages(messagesArray);
console.log('Changes: ', messages);
})
: console.log('No ID provided.');
}, [messages]);
If I run it with the dependency 'messages', It works, but the app gets very slow, so I placed a console.log to check the flow of the useEffect, and I figured it was on an infinite loop, looking for changes over and over. I assume this happens because whenever the useEffect runs, It 're sets' the messages array, so that causes it to run again.
I can solve the loop issue by removing 'messages' from the dependency array, but the problem is it won't render again when a new message is sent or received. So I'm not sure what I should do.
PS: The chat depends on the actual user ID, the condition at the start is to assure an ID is received, otherwhise it shouldn't query the firestore db.
I ran into this issue and the following seem to work. Like #Thor0o0 said, you would need a conditional statement to check if you already captured the message in your state.
I would use id as a dependency and not messages.
const [messages, setMessages] = useState([]);
const messagesRef = firestore().collection('messages');
useEffect(() => {
if(!id) return console.log('No ID provided.');
const messagesArray = messages;
messagesRef
.where('idRequest', '==', id)
.orderBy('createdAt')
.limit(25)
.get()
.then((response) => {
response.forEach((doc) => {
const foundMsgIndex = messagesArray.findIndex(msg=>msg.id===doc.id)
if(foundMsgIndex < 0){
messagesArray.push({...doc.data(), id:doc.id});
}
});
setMessages(messagesArray);
console.log('Changes: ', messages);
})
}, [id]);
You call SetMessages() inside the UseEffect, which changes messages, which triggers your useEffect.
Your linter should have caught this.
Check the code below.
const [messages, setMessages] = useState([]);
const messagesArray = [];
const messagesRef = firestore().collection('messages');
useEffect(() => {
id
? messagesRef
.where('idRequest', '==', id)
.orderBy('createdAt')
.limit(25)
.get()
.then((response) => {
response.forEach((doc) => {
//check here if the message is already in the messages array
//before pushing it and calling setMessages. Use findIndex or
//filter method to do so.
//something like this
const messageExists = messages.findIndex(doc.data());
if (!messageExists){
messagesArray.push(doc.data());
setMessages(messagesArray);
});
})
: console.log('No ID provided.');
}, [messages]);
Since you said you need to keep the messages array as a dependency of useEffect, what you can do is keep a copy of the messages array in a ref and compare it to the messages variable in your useEffect callback.
const [messages, setMessages] = useState([]);
const messagesArray = [];
const messagesRef = firestore().collection("messages");
const lastMessages = useRef();
useEffect(() => {
// Only do something if the messages variable was reassigned outside of the useEffect
if (messages !== lastMessages.current) {
id
? messagesRef
.where("idRequest", "==", id)
.orderBy("createdAt")
.limit(25)
.get()
.then((response) => {
response.forEach((doc) => {
messagesArray.push(doc.data());
});
// Update the value of the ref
lastMessages.current = messagesArray;
setMessages(messagesArray);
console.log("Changes: ", messages);
})
: console.log("No ID provided.");
}
}, [messages]);
This works because the value of a ref is preserved even when the component re-renders.
The only time you use messages in your useEffect is for console.log('Changes: ', messages) - which won't even be valid by that point.
Remove that console.log statement, and messages won't be a dependency any more. No more loop. id and messageArray are actual dependencies, and should be added; neither one is modified in the useEffect, so no loop.
IF you need the console.log('Changes: ', messages), don't do it in this useEffect - create another JUST FOR THE CONSOLE.LOG.
useEffect(() => {
console.log('Changes: ', messages);
}, [messages]);
this will only be called when messages changes, and doesn't modify messages, so no loop.

How to update state array fetched from API in React Hooks?

I'm fetching data from Studio Ghibli API and I am able to successfully fetch it, set the state array of objects and render it in my presentational component. However, I'm trying to create a function which will add new property "keyword" to every object in my state array. The problem is that when i try to copy the state array to manipulate it in my createKeywords function, the returned copy is empty and I'm unable to manipulate it after it being set.
This is the relevant code:
const baseUrl = 'https://ghibliapi.herokuapp.com/'
const [hasError, setErrors] = useState(false)
const [movies, setMovies] = useState([])
useEffect(() => {
fetch(baseUrl + 'films')
.then((res) => res.json())
.then((res) => {
console.log(res);
setMovies(res)
createKeywords()
})
.catch(err => setErrors(true));
}, [])
const createKeywords = () => {
const moviesWithKeywords = [...movies]
moviesWithKeywords.forEach(function(movie){
movie.keyword = 'castle'
});
setMovies(moviesWithKeywords)
}
If i don't call the createKeywords function everything works fine but obviously copying and setting new movies state creates problem. I tried adding [movies] instead of empty array in useEffect and that works but then useEffect runs indefinitely. Thank you in advance, React isn't my strong suit!
The solution seems might not be very obvious. There are cases where setMovies (in general setting the state) is an async operation, which means that even if you setMovies the movies variable is not being updated quite fast and therefore you are already executing the createKeawords function. This means that within the keywords function the movies variable didn't have the chance to update fast enough. I would recommend to pass the res as a parameter in the createKeywords and use this variable to copy the array to the moviesWithKeywords.
Have a look here under the section State Updates May Be Asynchronous
So do something like that:
const baseUrl = 'https://ghibliapi.herokuapp.com/'
const [hasError, setErrors] = useState(false)
const [movies, setMovies] = useState([])
useEffect(() => {
fetch(baseUrl + 'films')
.then((res) => res.json())
.then((res) => {
console.log(res);
setMovies(res)
createKeywords(res)
})
.catch(err => setErrors(true));
}, [])
const createKeywords = (movies) => {
const moviesWithKeywords = [...movies]
moviesWithKeywords.forEach(function(movie){
movie.keyword = 'castle'
});
setMovies(moviesWithKeywords)
}

Resources