Firebase's onSnapshot inside a useEffect; why does this infinitely loop? - javascript

I'm getting documents from a collection in Firebase and I was wondering if the following useEffect would loop. Is this good practice?
useEffect(() => {
const routineRef = collection(db, "routines", session?.user?.id!, currentRoutine.name);
const unsubscribe = onSnapshot(routineRef, (docsSnap) => {
setWeightsHistorySnapshot(docsSnap.docs);
// Infinite loop because this fills up my console
console.log("Current data: ", docsSnap.docs);
});
return () => unsubscribe();
}, [weightsHistorySnapshot, currentRoutine.name, session?.user?.id]);

From the docs of onSnapshot
An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document.
You set the state using setWeightsHistorySnapshot, and have weightsHistorySnapshot in your dependencies array, which in turn changes on every call. This causes the hook to run over and over.

Related

React: Component randomly doesn't refresh after state changes

I am working on a CRUD (express, mongoose, mongodb) which is mostly working as expected... except when it comes to rendering the entries after deleting X amount of them.
Not deleting
Sometimes I can delete 10 entries without any issues, until the component is empty, and other times I delete some entries and then the page just stop rending the updated data and enters an infinite loading state in the browser; only when I hard reload the browser the page renders the latest data, yet it does delete the entry , it just freezes it seems!
From React:
useEffect(() => {
queryClient.invalidateQueries(["allUsers"]);
});
const mutation = useMutation({
mutationFn: async (userid) => {
return await axios.delete("/api/deleteuser", { data: { userid: userid } });
},
});
const handleDelete = (userid) => {
mutation.mutate(userid);
navigate("/", { replace: true });
};
From Mongoose
const deleteUser = async (req, res) => {
try {
await newUser.findOneAndDelete({ userid: req.body.userid });
} catch (error) {
return error;
}
};
Tried invalidating the query cache at the delete function but the result is the same. It just happens randomly, as if the request was not fulfilled... in dev tools/network the request is never fulfilled but the data, whenever it does work, is updated.Network/pending
Edit: I'm using a mongoDB free account... I've read sometimes the response times are not the best, perhaps it is the reason?
useEffect without a dependency array is pretty pointless. It will run every time the component is rerendered.
I assume you are using react-query. Try moving your queryClient.invalidate to onSuccess or onSettled in your useMutation config object. This will ensure your query is invalidated only after the mutation is done.
https://react-query-v3.tanstack.com/guides/mutations#mutation-side-effects

How to stop executing a command the contains useState?

This is my code which sends a GET request to my backend (mySQL) and gets the data. I am using useState to extract and set the response.data .
const baseURL = 'http://localhost:5000/api/user/timesheet/13009';
const [DataArray , setDataArray] = useState([]);
axios.get(baseURL).then( (response)=>{
setDataArray(response.data);
});
But useState keeps on sending the GET request to my server and I only want to resend the GET request and re-render when I click a button or execute another function.
Server Terminal Console
Is there a better way to store response.data and if not how can I stop automatic re-rendering of useState and make it so that it re-renders only when I want to.
As pointed out in the comments, your setState call is triggering a re-render which in turn is making another axios call, effectively creating an endless loop.
There are several ways to solve this. You could, for example, use one of the many libraries built for query management with react hooks, such as react-query. But the most straightforward approach would be to employ useEffect to wrap your querying.
BTW, you should also take constants such as the baseUrl out of the component, that way you won’t need to include them as dependencies to the effect.
const baseURL = 'http://localhost:5000/api/user/timesheet/13009';
const Component = () => {
const [dataArray , setDataArray] = useState([]);
useEffect(() => {
axios.get(baseURL).then( (response)=>{
setDataArray(response.data);
});
}, []);
// your return code
}
This would only run the query on first load.
you have to wrap your request into a useEffect.
const baseURL = 'http://localhost:5000/api/user/timesheet/13009';
const [DataArray , setDataArray] = useState([]);
React.useEffect(() => {
axios.get(baseURL).then((response)=>{
setDataArray(response.data);
})
}, [])
The empty dependency array say that your request will only be triggered one time (when the component mount). Here's the documentation about the useEffect
Add the code to a function, and then call that function from the button's onClick listener, or the other function. You don't need useEffect because don't want to get data when the component first renders, just when you want to.
function getData() {
axios.get(baseURL).then(response => {
setDataArray(response.data);
});
}
return <button onClick={getData}>Get data</button>
// Or
function myFunc() {
getData();
}

How to pass a document fetched from firestore to local state in react

I have some documents in firebase firestore.
I am using this javascript function to fetch those
const fetchName = async () => {
console.log("fetchName fired");
const snapshot = await db
.collection("Users")
.doc(user?.uid)
.collection("details")
.doc("data")
.get();
// Data is a local state variable
// SetData is used to manipulate Data
setData(snapshot.data());
console.log("Snapped Data: ",snapshot.data());
console.log("Fetched Data: ",Data);
};
This is the state variable
const [Data, setData] = useState("");
I'm passing fetchName() function to useEffect hook.
useEffect(()=>
{
console.log("UseEffect Fired");
if(user!==null)
{
console.log("User not NUll, User->",user);
fetchName();
}
},[user])
The Issue
UseEffect is working correctly and firing fetchName() every time user logs in or logs out.
Here the main problem is When use effect is fired I can see snapshot.data() fetching the correct data but it is not assigning it to setData(). From hit and trial I have seen setData() is fired only the time I make any changes to my code or restart the server.
Desired Result
But I don't want that I want everytime snapshot.data() fetches the data it should assign it to Data using setData()
If you are worried about the 2nd line of console.log inside the fetchName, you're thinking in a wrong way.
So state updates are asynchronous.
For example,
const handleEvent = e => {
setState(e.target.value);
console.log(state);
}
This is not going to log the most updated value - this is happening because state updates are asynchronous, so synchronous behavior after a state update shouldn't rely on the state variable to get the most updated value for it.
For deeper explanation, check here and here

how to use setInterval with redux-thunk?

I'm using redux-thunk to manage async functions, and I want to use setInterval within an action creator, this is my code:
export const startLobbyPolling = () => dispatch => {
const pollTimer = setInterval(() => {
dispatch(fetchLobby);
}, POLL_TIME);
dispatch({ type: START_LOBBY_POLLING, payload: pollTimer });
};
fetchLobby is another action creator that simply fetch a request and store its data.
but surprisingly it's not working as it only shows START_LOBBY_POLLING action in redux debugger tool and nothing happens afterward. I would appreciate it to know how to use setInterval with redux.
I would suggest to not use redux as a polling manager.
Instead you should have a component / container that does that for you. The reason being is, that when the component will get unmounted, also your polling will be properly canceled
const PollContainer = ({ fetchLoby, children }) => {
useEffect(() => {
const ptr = setInterval(fetchLoby, POLL_TIME)
return () => clearInterval(ptr)
}, [fetchLoby])
return children
}
you can now use your PollContainer and as long as it is mounted, it will keep fetchLoby.
Seems like i should had called the function not just pass it as an argument to dispatch
dispatch(fetchLobby); -->
dispatch(fetchLobby()); and it works now
There's nothing particular about using setInterval with redux-thunk. It should work just as it would anywhere else. You need to see the callback function which has been used in the setInterval.

Firebase firestore onSnapshot - Get real-time updates after initial data query using promises

In order to show loading progress, I'm trying to wrap my onSnapshot call in a promise upon initial fetch. Data is loading correctly, but real-time updates are not functioning correctly.
Is there a way implement this type of functionality using the onSnapshot method?
Here's my initial data grab. Real-time updates functioned correctly before implementing the promise wrapper:
const [heroesArr, setHeroesArr] = useState([]);
const db = firebase.firestore();
const dbError = firebase.firestore.FirestoreError;
useEffect(() => {
const promise = new Promise((resolve, reject) => {
db.collection("characterOptions")
.orderBy("votes", "desc")
.onSnapshot(coll => {
const newHeroes = [];
coll.forEach(doc => {
const {
name,
votes
} = doc.data();
newHeroes.push({
key: doc.id,
name,
votes
});
});
if(dbError) {
reject(dbError.message)
} else {
resolve(newHeroes);
}
});
});
promise
.then(result => {
setHeroesArr(result);
})
.catch(err => {
alert(err);
});
}, [db]);
Again, data is being loaded to the DOM, but real-time updates are not functioning correctly.
onSnapshot is not really compatible with promises. onSnapshot listeners listen indefinitely, until you remove the listener. Promises resolve once and only once when the work is done. It doesn't make sense to combine onSnapshot (which doesn't end until you say) with a promise, which resolves when the work is definitely complete.
If you want do get the contents of a query just once, just get() instead of onSnapshot. This returns a promise when all the data is available. See the documentation for more details.
Here's what I think going on with your code: When your component mounts, your promise gets executed once via useEffect and that's where you set state. However, subsequent updates via the onSnapshot listener are not going to change the db reference, and therefore will not trigger useEffect again, and therefore will not execute the promise again, and therefore not set state again.
The only code that will execute when you receive a snapshot update is the callback function within .onSnapshot().
To fix this, I think you could try the following (I'm honestly not sure if it'll work, but worth a try):
Create a variable to track the initial load: let isInitialLoad = true;
Inside your promise.then(), add isInitialLoad = false;
Inside your .onSnapshot(), add if (!isInitialLoad) setHeroesArr(newHeroes); – this way, on initial load setHeroesArr gets executed on the promise but on snapshot updates setHeroesAss gets executed in the .onSnapshot() callback
The downside to this approach is that setHeroesArr will be called immediately upon a snapshot change rather than being wrapped in a promise.
Hope this helps!

Categories