UseEffect infinite loop with Firebase - reactjs

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.

Related

Using a POST request's response values in React Native

everyone. I'm a new developer and this is my first post here, so please bear with me.
What I'm trying to do is access the values that I received from a POST request that I made.
Below you can see what I'm trying to do. I set up a state where I'll store my data and then there's a function called fetchData that does a POST request and receives an object with the values, right?
Now I that I've received my object, I want to store its values in some of my variables.
Since the values have been stored in the 'data' state, I thought I would access its values by doing something like 'userName = data.userName', or something like 'age = data.userAge'. Obviously that didn't work out because my IDE does not know those values yet lol. So how do you think I should access them?
const [data, setData] = useState([{}]);
useEffect(() => {
fetchData({
ids: [0],
})
.then((response: any) => {
setData(response);
})
.catch((error: any) => {
console.log('error', error);
});
}, []);
dont place the function directly in useEffect, do something like this instead
const [data, setData] = useState([])
const getData = async() => {
try{
const response = await fetchData({ids: [0]});
setData(response);
}catch(error){
console.log(error);
}
}
useEffect(() => {
getData();
},[])

useEffect is causing infinite loop when use state as dependency

Here simply I am fetching data from mysql DB and storing it in state and in order to fetch this data:
const [orders, setOrders] = useState([]);
To fetch data I am using different functions and finally I am calling those functions using useEffect simple enough and so for everything is working perfectly but the problem comes whenever I use the state as dependency where I am storing data beacause if I dont do that then I have to manually refresh the page for latest changes and I have tried every given solution on stackoverflow but any of the solution didnt work so someone can please help me how can I use this state as dependencey without causing infinite loop:
const [orders, setOrders] = useState([]);
const loadData = async () => {
const response = await fetch("http://localhost/k-shop/load.php");
const result = await response.json();
setOrders(result);
};
const loadTotal = async () => {
const response = await fetch("http://localhost/k-shop/amount.php");
const result = await response.json();
setTotal(result);
};
useEffect(() => {
loadData();
loadTotal();
}, [orders]);
console.log(orders);
If you move the state into the useEffect dependency, you can then check if it is empty, and only set it when that check passes.
It will set the state once to populate and not pass the check again.
const [orders, setOrders] = useState([]);
const loadData = async () => {
const response = await fetch("http://localhost/k-shop/load.php");
const result = await response.json();
setOrders(result);
};
const loadTotal = async () => {
const response = await fetch("http://localhost/k-shop/amount.php");
const result = await response.json();
setTotal(result);
};
useEffect(() => {
if(orders.length === 0) {
loadData();
}
// you can do the same with checking loadTotal() state
}, [orders]);
console.log(orders);
Avoid ,non-primitive data types in dependencyArray ,
useEffect(() => {
loadTotal();
loadData();
}, [total, orders.length]);
every times you "setOrders" means you change the state,every times you change the state,means the "useEffect" will do again.that cause infinite loops.why not try useEffect(() => {loadData()}, [])?

react useEffect strange behaviour - can't explain?

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();
}, [])

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

useEffect not triggering but the template is being rendered somehow

I am getting too many re-renders while using react-hooks.
I am trying to fetch data from api by using a parameter in URL.
Here's the code:
export default function Details() {
const { title } = useParams();
const [loading, setLoading] = useState(true);
const [details, setDetails] = useState([]);
const [error, setError] = useState("");
function getDetails(keyword) {
if (keyword) {
setLoading(true);
fetch(
`API`
)
.then((res) => {
let result = res.data.results;
result = result.filter(function (result) {
return (result.title = keyword);
});
setDetails(result[0]);
setLoading(false);
console.log(details);
})
.catch((err) => {
setError(["Unable to fetch data"]);
setLoading(false);
});
}
}
getDetails(title)
return(
// template
)
now I think this is happening at the line where I call getDetails.
then I tried using useEffect to load the data only once after it is mounted,
useEffect(() => {
getDetails(title);
}, []);
It still is unable to fetch the data, as the getDetails function is never called.
How can I resolve this?
Edit:
Fixed one silly error.
Here's the codesandbox link:
Codesandbox
There are multiple issues with this, first you need to specify what you want to be notified about when the useEffect gets called again. You could do this by adding the variables you want within the array
useEffect(() => {
getDetails(title);
}, [
// Add what you want here
]);
The second issue you have is that you declared the detalis variable twice. Once using the set state here: const [details, setDetails] = useState([]);
The second time here:
const details = getDetails(title)
the code here has two obvious error beside the functionality problems you mentioned:
1 - you cannot declare two variables with same name using let or const; it will throw a SyntaxError
const [details, setDetails] = useState([]);
...
const details = getDetails(title)
2- getDetails function is written with a asynchronous mindset, and it will return nothing,
so details in const details = getDetails(title) will be set to undefined
Looks like your getDetails function has title param so I would add title and getDetails both in the dependency list of useEffects
Or place getDetails inside the useEffect
Here is your working code. You had multiple problems where res.data was undefined so you need to get res.results directly based on your response object
useEffect(() => {
function getDetails(keyword) {
if (keyword) {
setLoading(true);
fetch(
`https://api.jikan.moe/v3/search/anime?q=${keyword}&page=1&genre_exclude=0`
)
.then((res) => res.json())
.then((res) => {
console.log(res.results);
let result = res.results;
console.log(result);
result = result.filter(function (result) {
return (result.title = keyword);
});
setDetails(result[0]);
setLoading(false);
console.log(3);
})
.catch((err) => {
console.log(err);
setError(["Unable to fetch data"]);
setLoading(false);
});
}
}
console.log('calling getDetails')
getDetails(title);
}, [title]);
Note: tested in the code sandbox link provided in the question. Its working code.

Resources