Stale State in useEffect and setInterval - reactjs

So I am working on an app, which as a main functionality uses a timer.
However as the timer should not be user manipulated it shall run on the backend, this is being achieved with firebase cloud functions and server timestamps.
Currently I am using an useEffect Hook on the Page which intializes the Timer with setInterval and an empty dependency Array so that the timer gets only Started once on Component mount. In the setInterval Callback there is a function called "getPoolTime", this async function calls a cloudFunction that writes a new serverTimestamp to firestore and calculates the remaining time, based on other factors. (Probably not the most efficient way to do this, but it was the only way I could figure out for now xD ).
The problem I have now is, even if the timer ran out, setInterval still calls "getPoolTime" which invokes a cloud function and this is not very efficient. I want the cloud function not being invoked when the timer ran out.
I tried:
if Statement in the useEffect Hook, so when the timer state is say < 1 do not call "getPoolTime" anymore, however as the the timer only mounts at component mount, there is the problem of stale state and therefore this does not work.
If Statement in the "getPoolTime" function, which leads to the problem that once the timer has reached zero it can never be restarted as even if the function is invoked the state is < 1 doesn't pass the if check and the new time is never calculated.
So I guess the solution I need is, something that starts the timer and calls the "getPoolTime" on mount until timerDisplay < 1 and on mount calls "getPoolTime" once again.
Thank you for your help - I am well aware that what I am doing is probably really inefficient and there are certainly better ways to do it - if you look at my code and have an idea how I could make it better, let me know - I am eager to learn!
const [timerDisplay, setTimerDisplay] = useState(0);
async function getPoolTime() {
try {
const setWriteTimestamp = httpsCallable(functions, "setWriteTimestamp");
setWriteTimestamp({ slug: slug });
const poolRef = doc(db, "pools", slug);
const poolSnap = await getDoc(poolRef);
const timeLeft =
poolSnap.data().expTimestamp.toMillis() / 1000 -
poolSnap.data().writeTimestamp.toMillis() / 1000;
console.log(timeLeft);
setTimerDisplay(Math.floor(timeLeft));
} catch (error) {
console.log("Error occured in getting Pool Time", error);
}
}
useEffect(() => {
const interval = setInterval(() => {
getPoolTime();
}, 1100);
return () => {
clearInterval(interval);
};
}, []);

Related

Issue clearing a recursive timeout with onClick in React

I'm rebuilding a special type of metronome I built in vanilla js, with React. Everything is working, except when a user clicks the 'STOP' button, the metronome doesn't stop. It seems I'm losing the timeout ID on re-renders, so clearTimeout is not working. This is a recursive timeout, so it calls itself after each timeout acting more like setInterval, except for it's adjusting the interval each time, thus I had to use setTimeout.
I've tried to save the timeoutID useing setState, but if I do that from within the useEffect hook, there's an infinite loop. How can I save the timerID and clear it onClick?
The code below is a simplifed version. The same thing is on codepen here. The codepen does not have any UI or audio assets, so it doesn't run anything. It's just a gist of the larger project to convey the issue.
You can also view the vanilla js version that works.
import { useState, useEffect } from 'React';
function startStopMetronome(props) {
const playDrum =
new Audio("./sounds/drum.wav").play();
};
let tempo = 100; // beats per minute
let msTempo = 60000 / tempo;
let drift;
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let timeout;
let expected;
const round = () => {
playDrum();
// Increment expected time by time interval for every round after running the callback function.
// The drift will be the current moment in time for this round minus the expected time.
let drift = Date.now() - expected;
expected += msTempo;
// Run timeout again and set the timeInterval of the next iteration to the original time interval minus the drift.
timeout = () => setTimeout(round, msTempo - drift);
timeout();
};
// Add method to start metronome
if (isRunning) {
// Set the expected time. The moment in time we start the timer plus whatever the time interval is.
expected = Date.now() + msTempo;
timeout = () => setTimeout(round, msTempo);
timeout();
};
// Add method to stop timer
if (!isRunning) {
clearTimeout(timeout);
};
});
const handleClick = (e) => {
setIsRunning(!isRunning);
};
return (
<div
onClick={handleClick}
className="start-stop"
children={isRunning ? 'STOP' : 'START'}>
</div>
)
}
Solved!
First, my timeouts didn't need the arrow functions. They should just be:
timeout = setTimeout(round, msTempo);
Second, a return in the useEffect block executes at the next re-render. The app will re-render (i thought is would be immediate). So, I added...
return () => clearTimeout(timeout);
to the bottom of the useEffect block.
Lastly, added the dependencies for my useEffect block to ensure it didn't fire on the wrong render.
[isRunning, subdivisions, msTempo, beatCount]);

Perform useState before next setInterval call

I'm working on a console component that retrieves logs from a server. To prevent the console from being overcrowded, I implemented a function that clears the console output. However, the clearing is overwritten by the next setInterval call.
const [logs, setLogs] = useState([])
const saveLogs = (newLogs) => {
setInterval(() => setLogs(newLogs), 2000);
};
const clearConsole = () => {
setLogs([])
};
logs is an array of strings and I'm appending a new log every time I receive a new log from the server.
Using clearInterval() before updating the state would solve the problem, but as I intend to use multiple tabs for the console, I would have to clear the interval of every instance, which is not what I want.

Next JS + Supabase real time subscription subscribes with state "closed"

I am working on this helpdesk for a school project using Next JS and Supabase and got stuck on realtime chat between the operator and the client.
I subscribe to the table in useEffect hook and return the unsubscribe function to clean up.
But when i change tickets sometimes the subscription is established but with a state already closed which causes the subscription to stop sending the callbacks.
I think the problem might be with the new subscription being called right after (or maybe even during) the cleanup function which causes even the new one to be closed. But I am not sure how to get around that.
Any ideas?
this is the useEffect used:
useEffect(() => {
getMessages(id)
const MessageSubscription = supabase
.from<definitions['messages']>('messages')
.on('INSERT', (message) => {
getMessages(id)
})
.subscribe()
async function removeMessageSubscription() {
await supabase.removeSubscription(MessageSubscription)
}
return () => {
removeMessageSubscription()
}
}, [])
Probably useEffect fires twice and it may occure some unexpected behaviors for realtime events.
Just disable the strict mode and try again.
// next.config.js
module.exports = {
reactStrictMode: false,
}

React useEffect and setInterval

I'm making a React dashboard that calls an API every minute for updates. Following the many answers in SO, I have this at the moment that sort of works:
const Dashboard = (props) => {
const [stockData, setStockData] = useState([]);
useEffect(() => {
//running the api call on first render/refresh
getAPIData();
//running the api call every one minute
const interval = setInterval(() => {
getAPIData()
}, 60000);
return () => clearInterval(interval);
}, []);
//the api data call
const getAPIData = async () => {
try {
const stdata = await DataService.getStockData();
setStockData(stdata);
}
catch (err) {
console.log(err);
}
};
However I keep getting browser warning
React Hook useEffect has a missing dependency: 'getAPIData'. Either include it or remove the dependency array
Is this a cause for concern (e.g. causing memory leaks)?
Ive tried to fix it:
It doesnt seem possible to not use useEffect (use setInterval directly)
If I remove dependency or put stockData as dependency for useEffect,
I'll see the API call being made every second, so I assume thats not
right.
If I put the api data call block directly in useEffect, the page wont
have the data shown when it loads the first time/or the page refreshed and I have to wait
for a minute.
I found several references on the issue such as
here
and
here
but I couldn't comprehend it given that I've only started using React
a month ago.
Appreciate any help for this!
You can resolve this issue in multiple way:
You can put getApiData in useEffect direct and use it...
You can use useCallBack, useEffect is go to re-render and make mempry leeek issue since every time react render Dashboard its re-create the getAPIData, you can prevent this case by using useCallBack, and you must make sure about dependency, just you need to put what you need...for example:

Infinite Loop with useEffect - ReactJS

I have a problem when using the useEffect hook, it is generating an infinite loop.
I have a list that is loaded as soon as the page is assembled and should also be updated when a new record is found in "developers" state.
See the code:
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = async () => {
await api.get('/developers').then(response => {
setDevelopers(response.data);
});
};
// This way, the loop does not happen
useEffect(() => {
getDevelopers();
}, []);
// This way, infinte loop
useEffect(() => {
getDevelopers();
}, [developers]);
console.log(developers)
If I remove the developer dependency on the second parameter of useEffect, the loop does not happen, however, the list is not updated when a new record is found. If I insert "developers" in the second parameter of useEffect, the list is updated automatically, however, it goes into an infinite loop.
What am I doing wrong?
complete code (with component): https://gist.github.com/fredarend/c571d2b2fd88c734997a757bac6ab766
Print:
The dependencies for useEffect use reference equality, not deep equality. (If you need deep equality comparison for some reason, take a look at use-deep-compare-effect.)
The API call always returns a new array object, so its reference/identity is not the same as it was earlier, triggering useEffect to fire the effect again, etc.
Given that nothing else ever calls setDevelopers, i.e. there's no way for developers to change unless it was from the API call triggered by the effect, there's really no actual need to have developers as a dependency to useEffect; you can just have an empty array as deps: useEffect(() => ..., []). The effect will only be called exactly once.
EDIT: Following the comment clarification,
I register a developer in the form on the left [...] I would like the list to be updated as soon as a new dev is registered.
This is one way to do things:
The idea here is that developers is only ever automatically loaded on component mount. When the user adds a new developer via the AddDeveloperForm, we opportunistically update the local developers state while we're posting the new developer to the backend. Whether or not posting fails, we reload the list from the backend to ensure we have the freshest real state.
const DevList: React.FC = () => {
const [developers, setDevelopers] = useState<DevelopersData[]>([]);
const getDevelopers = useCallback(async () => {
await api.get("/developers").then((response) => {
setDevelopers(response.data);
});
}, [setDevelopers]);
useEffect(() => {
getDevelopers();
}, [getDevelopers]);
const onAddDeveloper = useCallback(
async (newDeveloper) => {
const newDevelopers = developers.concat([newDeveloper]);
setDevelopers(newDevelopers);
try {
await postNewDeveloperToAPI(newDeveloper); // TODO: Implement me
} catch (e) {
alert("Oops, failed posting developer information...");
}
getDevelopers();
},
[developers],
);
return (
<>
<AddDeveloperForm onAddDeveloper={onAddDeveloper} />
<DeveloperList developers={developers} />
</>
);
};
The problem is that your getDevelopers function, calls your setDevelopers function, which updates your developers variable. When your developers variable is updated, it triggers the useEffect function
useEffect(() => {
getDevelopers();
}, [developers]);
because developers is one of the dependencies passed to it and the process starts over.
Every time a variable within the array, which is passed as the second argument to useEffect, gets updated, the useEffect function gets triggered
Use an empty array [] in the second parameter of the useEffect.
This causes the code inside to run only on mount of the parent component.
useEffect(() => {
getDevelopers();
}, []);

Resources