Save a function in state and update it periodically - reactjs

The getDocsRealtime() adds a Firebase listener for a query and returns an unsubscribe function. In componentDidMount, this function loads top 5 documents initially. Every time the page is scrolled to bottom, this function is executed again to get the next 5 documents, and returns another unsubscribe function for the new documents. I want to make it so that:
unsubscribe function = old unsubscribe function + new unsubscribe function
Here is my code:
componentDidMount = async () => {
let unsubscribe = await getDocsRealtime()
this.setState({ unsubscribe })
// Add Listener when page is scrolled to bottom
window.addEventListener('scroll', this.onScrollToBottom)
}
onScrollToBottom = async () => {
if (window.scrollY > placeholderTop) {
let unsubscribe = await getDocsRealtime()
unsubscribe = () => {
this.state.unsubscribe()
unsubscribe()
}
this.setState({ unsubscribe })
}
}
componentWillUnmount = () => {
// Unsubscribe Listener
this.state.unsubscribe && this.state.unsubscribe
// Remove Listener for when page is scrolled to bottom
window.removeEventListener('scroll', this.onScrollToBottom)
}
The problem I am having here is that instead of calling the old unsubscribe function, the program is calling the unsubscribe function stored in the state and I get RangeError: Maximum call stack size exceeded error.

Every time the page is scrolled to bottom, this function is executed
again to get the next 5 documents
Since you want to query the database each time a specific event occurs in your front-end, i.e. the scrolling to the bottom, you should not, IMO, use a listener to query the database.
I would suggest you paginate the queries by "combining query cursors with the limit() method" as explained in the doc. With this technique, each time the user scrolls down, you construct a new query starting at the last previously visible document and you execute it once (instead of continually "listening to it").
I understand that it seems interesting to combine realtime listening to the DB with pagination but in reality it can create weird situations when a document is shown twice in the front end because a new document has been added in the middle of a set of previously displayed docs.

Related

when changing deps, useEffect re-runs the setInterval inside itself. how to avoid?

There is a web application - a simple widget that loads local time from the WorldTime API for different regions.
The task is to fetch time from the server every five seconds.
To do this, I used setInterval inside useEffect , in which I put the function where fetch occurs. However, there is a problem - when selecting / changing the region, I have to wait five seconds until setInterval completes.
setInterval, as I understand it, is restarted inside useEffect when deps changes, when a new region is selected.
How can this problem be solved?
App on github: https://mmoresun.github.io/live-clock/
The code itself: https://codesandbox.io/s/codepen-with-react-forked-66iz3n?file=/src/App.js
You can call directly the same getTime function before setting the interval
useEffect(() => {
getTime(value);
const myInterval = setInterval(() => {
// refreshing with setInterval every 5 seconds
getTime(value);
}, 5000);
return () => clearInterval(myInterval);
}, [value]); /
As for your codepen example, you are continuosly calling the timezone api, because you fetch it inside the SearchPanel component. That's wrong, remove it.

Reactjs Screen not refreshing, always one loop behind when updating

I am updating a record that is being displayed on the screen. Yet once the record is updated in the MySql database, the screen does not reflect the update. I am seeking a way to force the refresh. In the code below, I update the record and reload the array and filter the results. Yet the screen does not get updated. If I click the browsers refresh, the screen displays with the updated data. If I do a second or third update, it always shows the results from the prior update, always one update behind.
I am new to Reactjs so I am hoping this is obvious to someone.
...
seqUp(contacttoassetrelationshipid){
ContactAssetMasterService.getContactToAssetRelationshipById(contacttoassetrelationshipid).then( (res) =>{
let contacttoassetrelationship = res.data;
contacttoassetrelationship.contacttoassetRelationshipSequence = contacttoassetrelationship.contacttoassetRelationshipSequence + 1;
ContactAssetMasterService.updateContactToAssetRelationship(contacttoassetrelationship, contacttoassetrelationshipid);
});
ContactAssetMasterService.getContactToAssetRelationship().then((res)
=> {
this.setState({ contacttoassetrelationship: res.data});
this.setState({contacttoassetrelationship: this.state.contacttoassetrelationship.filter(contacttoassetrelationship
=> contacttoassetrelationship.assets_assetsid === this.props.match.params.assetsid)});
}); };
...
There are 2 problems with your code.
updateContactToAssetRelationship returns a promise, which is an asynchronous operation. Just like
you did by calling then for getContactToAssetRelationshipById
and getContactToAssetRelationship, you need to wait until it
finishes before fetching the updated data. Otherwise, execution
continues right away (without waiting update call to finish) and
getContactToAssetRelationship fetches out-dated data.
setState is an asynchronous operation too. Plus, multiple calls of setState merged into one batch call. In the following case, for example, count will be updated only once:
// assume this.state.count === 0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
// count will be 1
Final code (removed/renamed variables just for brievity):
seqUp(contacttoassetrelationshipid){
ContactAssetMasterService.getContactToAssetRelationshipById(contacttoassetrelationshipid).then((res) =>{
res.data.contacttoassetRelationshipSequence++;
ContactAssetMasterService.updateContactToAssetRelationship(res.data, contacttoassetrelationshipid).then(() => {
ContactAssetMasterService.getContactToAssetRelationship().then((res1) => {
this.setState({contacttoassetrelationship: res1.data.filter(c => c.assets_assetsid === this.props.match.params.assetsid)});
});
});
});
};

Why do I need cleanup in useEffect in react

Im new to react and Im not sure why do we need a cleanup function when dealing with EventListeners, Iam trying to set an event when resizing my window but i only have 1 event in (Elements -> Event Listeners) tab in chrome dev tools, even if I don't return a cleanup function in my hook
Heres my code:
useEffect(() => {
window.addEventListener("resize", function checksize() {
console.log("1");
});
});
First of all, you should absolutely avoid using that in your code, because on each rerender, it is going to add a new event listener to the window.
Secondly, to answer your question, you should have a cleanup effect to remove event listeners and other similar mechanisms to avoid memory leaks. If you don't clean them up, then you leave dangling eventlisteners taking up memory which is not a good idea, and may be picked up on within your other components as well. So the ideal way to handle this kind of code is
const logOne = () => console.log("1"); //put the function reference in a variable so that you can remove the event afterwards
useEffect(() => {
window.addEventListener("resize", logOne);
return () => {
window.removeEventListener("resize", logOne); //remove event listener on unmount
}
}, []); //empty dependency array so that function only runs on first render, so that consequent rerenders don't cause you to add more of these event listeners
Because you will keep adding listeners every render. Instead of printing "1" once, it will print it twice next time. Then 3 times on the next re-render and so on.

Issues accessing react state in firestore onSnapshot listener

I want to wait to apply state updates from the back-end if a certain animation is currently running. This animation could run multiple times depending on the game scenario. I'm using react-native with hooks and firestore.
My plan was to make an array that would store objects of the incoming snapshot and the function which would use that data to update the state. When the animation ended it would set that the animation was running to false and remove the first item of the array. I'd also write a useEffect, which would remove the first item from the array if the length of the array had changed.
I was going to implement this function by checking whether this animation is running or whether there's an item in the array of future updates when the latest snapshot arrives. If that condition was true I'd add the snapshot and the update function to my array, otherwise I'd apply the state update immediately. I need to access that piece of state in all 3 of my firestore listeners.
However, in onSnapshot if I try to access my state it'll give me the initial state from when the function rendered. The one exception is I can access the state if I use the function to set the state, in this case setPlayerIsBetting and access the previous state through the function passed in as a callback to setPlayerIsBetting.
I can think of a few possible solutions, but all of them feel hacky besides the first one, which I'm having trouble implementing.
Would I get the future state updates if I modify the useEffect for the snapshots to not just run when the component is mounted? I briefly tried this, but it seems to be breaking the snapshots. Would anyone know how to implement this?
access the state through calling setPlayerIsBetting in all 3 listeners and just set setPlayerIsBetting to the previous state 99% of the time when its not supposed to be updated. Would it even re-render if nothing is actually changed? Could this cause any other problems?
Throughout the component lifecycle add snapshots and the update functions to the queue instead of just when the animation is running. This might not be optimal for performance right? I wouldn't have needed to worry about it for my initial plan to make a few state updates after an animation runs since i needed to take time to wait for the animation anyway.
I could add the state I need everywhere on the back-end so it would come in with the snapshot.
Some sort of method that removes and then adds the listeners. This feels like a bad idea.
Could redux or some sort of state management tool solve this problem? It would be a lot of work to implement it for this one issue, but maybe my apps at the point where it'd be useful anyway?
Here's my relevant code:
const Game = ({ route }) => {
const [playerIsBetting, setPlayerIsBetting] = useState({
isBetting: false,
display: false,
step: Infinity,
minimumValue: -1000000,
maximumValue: -5000,
});
const [updatesAfterAnimations, setUpdatesAfterAnimations] = useState([]);
// updatesAfterAnimations is currently always empty because I can't access the updated playerIsBetting state easily
const chipsAnimationRunningOrItemsInQueue = (snapshot, updateFunction) => {
console.log(
"in chipsAnimationRunningOrItemsInQueue playerIsBetting is: ",
playerIsBetting
); // always logs the initial state since its called from the snapshots.
// So it doesn't know when runChipsAnimation is added to the state and becomes true.
// So playerIsBetting.runChipsAnimation is undefined
const addToQueue =
playerIsBetting.runChipsAnimation || updatesAfterAnimations.length;
if (addToQueue) {
setUpdatesAfterAnimations((prevState) => {
const nextState = cloneDeep(prevState);
nextState.push({ snapshot, updateFunction });
return nextState;
});
console.log("chipsAnimationRunningOrItemsInQueue returns true!");
return true;
}
console.log("chipsAnimationRunningOrItemsInQueue returns false!");
return false;
};
// listener 1
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
const unsubscribeFromPlayerCards = db
.collection("tables")
.doc(tableId)
.collection("players")
.doc(player.uniqueId)
.collection("playerCards")
.doc(player.uniqueId)
.onSnapshot(
function (cardsSnapshot) {
if (!chipsAnimationRunningOrItemsInQueue(cardsSnapshot, updatePlayerCards)) {
updatePlayerCards(cardsSnapshot);
}
},
function (err) {
// console.log('error is: ', err);
}
);
return unsubscribeFromPlayerCards;
}, []);
};
// listener 2
useEffect(() => {
const tableId = route.params.tableId;
const db = firebase.firestore();
const unsubscribeFromPlayers = db
.collection("tables")
.doc(tableId)
.collection("players")
.onSnapshot(
function (playersSnapshot) {
console.log("in playerSnapshot playerIsBetting is: ", playerIsBetting); // also logs the initial state
console.log("in playerSnapshot playerIsBetting.runChipsAnimation is: "playerIsBetting.runChipsAnimation); // logs undefined
if (!chipsAnimationRunningOrItemsInQueue(playersSnapshot, updatePlayers)) {
updatePlayers(playersSnapshot);
}
},
(err) => {
console.log("error is: ", err);
}
);
return unsubscribeFromPlayers;
}, []);
// listener 3
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
// console.log('tableId is: ', tableId);
const unsubscribeFromTable = db
.collection("tables")
.doc(tableId)
.onSnapshot(
(tableSnapshot) => {
if (!chipsAnimationRunningOrItemsInQueue(tableSnapshot, updateTable)) {
updateTable(tableSnapshot);
}
},
(err) => {
throw new err();
}
);
return unsubscribeFromTable;
}, []);
I ended up not going with any of the solutions I proposed.
I realized that I could access the up to date state by using a ref. How to do it is explained here: (https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559) And this is the relevant code sample from that post: (https://codesandbox.io/s/event-handler-use-ref-4hvxt?from-embed)
Solution #1 could've worked, but it would be difficult because I'd have to work around the cleanup function running when the animation state changes. (Why is the cleanup function from `useEffect` called on every render?)
I could work around this by having the cleanup function not call the function to unsubscribe from the listener and store the unsubscribe functions in state and put them all in a useEffect after the component mounts with a 2nd parameter that confirmed all 3 unsubscribe functions had been added to state.
But if a user went offline before those functions were in state I think there could be memory leaks.
I would go with solution #1: In the UseEffect() hooks you could put a boolean flag in so the snapshot listener is only set once per hook. Then put the animation state property in the useEffect dependency array so that each useEffect hook is triggered when the animation state changes and you can then run whatever logic you want from that condition.

fuction not returning a list of radio button Reactjs- ReactBootstrap

I am working on a dash board, that fetches data from acuity scheduling.
I am making a form and using a function to get list of radio button:
following array has it, just to clarify. It takes time to get the value in from the API so I have used setTimeout, in the function:
setTimeout(() => {
return timeForID.map( obj => {
return (<Radio value={obj.date}>{obj.hours}:{obj.mins}</Radio>);
})
}, 500)
I am getting a blank space in the place of radio buttons.
There are a lot of answers in JavaScript out there about working with the event loop and callbacks -- See: How do I return the response from an asynchronous call?
Essentially, your return inside of setTimeout doesn't go anywhere.
In order to trigger another render in your component, you will have to use setState. You can call setState after your API call completes -- you shouldn't have to use setTimeout. Let's say you're using fetch to get the API:
fetch(apiUrl).then(response => response.json()).then(dates => setState({ dates }))
Now in your render function you can have:
{this.state.dates.map(({ date, hours, mins }) => (
<Radio value={date}>{hours}:{mins}</Radio>
)}
Initialize the dates property of state to an empty array to prevent errors on the initial load.

Resources