Interval not causing re-render in react component - reactjs

I am using the useEffect react hook to set an interval for a countdown variable. I have noticed that when the tab in my browser isn't active, the internal stops working. If the tab is active the code works exactly as expected.
Is there a better way to do this? Below is an example of the code.
export default function CountdownTimer({ minutes_left, action }) {
const [timeLeft, setTimeLeft] = useState();
function updateTimer() {
const remaining = ...work out time left
setTimeLeft(remaining);
}
useEffect(() => {
const interval = setInterval(() => {
updateTimer();
if (timeLeft.asMilliseconds() <= 0) {
action();
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<p>{timeLeft}</p>
);
}
I have implemented solutions in Dan Abramov's post here about this problem. However Dan's solution's also don't render when the tab is inactive.

Note that this is how browsers work: inactive tabs are deprioritised and timers get heavily throttled. Remember that setTimeout and setInterval are most expressly not "run code once X ms have passed", they are "run code after at least X ms have passed" without any guarantee that things won't actually happen much, much later due to scheduling (e.g. whether the tab is active, or even visible as far as the OS can inform the browser) or even just because a bit of JS code took longer than your indicated interval to complete and so your timeout literally can't fire until it's done (because JS is single-threaded).
For Firefox, See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API#Policies_in_place_to_aid_background_page_performance on what kind of throttling is performed on timers, and for Chrome, see https://developers.google.com/web/updates/2017/03/background_tabs
(For any other browser, a web search should find you similar documentation.)
However, think about what it means for a tab to become a background process: if your users can't see your tab, why would you need to update anything at all? Your users won't be able to see those updates. Instead, consider suspending purely visual tasks when a tab becomes invisible, and then resuming them when the tab becomes visible again, and using a clock-time check to determine whether "things need to happen".
In fact, this is important to do even on a foreground tab if you use setInterval or setTimeout: 10 consecutive setTimeout(..., 100) do not imply that it'll be exactly 1 second later once they're done, it means it's at least 1s later when we're done but we could easily reach that "one second later" mark by the time the 7th or 8th timeout fires because JS is single threaded, and if some task took a while, your timeout won't fire until that task is done. So you're going to at the very least need to update your code for that already anyway.

Probably because useEffect call use clearInterval function after the first render. Here's how the cleanup for useEffect works with the returned function - Example Using Hooks.

Related

react - wait for side effects to be cleaned before unmounting component

Is there a way to wait for the useEffect clean-up function to finish ?
useEffect(() => {
return async () => {
dialog({show: true, title: 'Cleaning up the mess. Please wait.'});
// Start a series of long running tasks
await system.killProcess();
await pollUntilProcessDoesNotExist(); // Do not go anywhere until this is done
dialog({show: false, title: undefined });
};
}, [selectedSequenceId]);
My question comes as the result of handling state when BE tasks take long time.
In my example, we have a system that does long time operations. When performing a long time operation it cannot do any other one. Trying to make system do other stuff will come as 409 errors.
Because of this, I would like to know if we can wait until a clean-up function is done. If it is not possible, I would use a transitional route to wait in there until system is free.
More ideas are very welcome.
In general you don't want to block unmounting of UI, which tells me that unmount is the incorrect dependency for the effect that you've described. What is it that causes the unmount? A back button press or something like that? That button should instead kick off the long running task and then do the navigation.

ReactJS - Inefficient useEffect runs four times

I have a useEffect function that must wait for four values to have their states changed via an API call in a separate useEffect. In essence the tasks must happen synchronously. The values must be pulled from the API and those stateful variables must be set and current before the second useEffect can be called. I am able to get the values to set appropriately and my component to render properly without doing these tasks synchronously, I have a ref which changes from true to false after first render (initRender), however I find the code to be hacky and inefficient due to the fact that the second useEffect still runs four times. Is there a better way to handle this?
//Hook for gathering group data on initial page load
useEffect(() => {
console.log("UseEffect 1 runs once on first render");
(async () => {
const response = await axios.get(`${server}${gPath}/data`);
const parsed = JSON.parse(response.data);
setGroup(parsed.group);
setSites(parsed.sites);
setUsers(parsed.users);
setSiteIDs(parsed.sitesID);
setUserIDs(parsed.usersID);
})();
return function cleanup() {};
}, [gPath]);
//Hook for setting sitesIN and usersIN values after all previous values are set
useEffect(() => {
console.log("This runs 4 times");
if (
!initRender &&
sites?.length &&
users?.length &&
userIDs !== undefined &&
siteIDs !== undefined
) {
console.log("This runs 1 time");
setSitesIN(getSitesInitialState());
setUsersIN(getUsersInitialState());
setLoading(false);
}
}, [sites, siteIDs, users, userIDs]);
EDIT: The code within the second useEffect's if statement now only runs once BUT the effect still runs 4 times, which still means 4 renders. I've updated the code above to reflect the changes I've made.
LAST EDIT: To anyone that sees this in the future and is having a hard time wrapping your head around updates to stateful variables and when those updates occur, there are multiple approaches to dealing with this, if you know the initial state of your variables like I do, you can set your dependency array in a second useEffect and get away with an if statement to check a change, or multiple changes. Alternatively, if you don't know the initial state, but you do know that the state of the dependencies needs to have changed before you can work with the data, you can create refs and track the state that way. Just follow the examples in the posts linked in the comments.
I LIED: This is the last edit! Someone asked, why can't you combine your different stateful variables (sites and sitesIN for instance) into a single stateful variable so that way they get updated at the same time? So I did, because in my use case that was acceptable. So now I don't need the 2nd useEffect. Efficient code is now efficient!
Your sites !== [] ... does not work as you intend. You need to do
sites?.length && users?.length
to check that the arrays are not empty. This will help to prevent the multiple runs.

React useRef scrolling just working within setTimeout

I'm trying to use scrollIntoView on a react application, and therefore in a page I am using this
useEffect(() => {
myRef.current.scrollIntoView({ behavior: 'smooth' });
}, [myRef, selectedSectionIndex, modalOpen]);
It works on almost every situation as I have the ref={myRef} associated to the element that I want to put into view. However, in one of the transitions even if the code is called it does nothing (not even throws an error). Only if I change it to
useEffect(() => {
setTimeout(() => myRef.current.scrollIntoView({ behavior: 'smooth' }), 0);
}, [myRef, selectedSectionIndex, modalOpen]);
I have read about similar issues, and that's why I actually tried this solution. But can't really put my finger on how this solves it. I keep reading the referenced dom element might not be rendered yet when the first code runs, but
a. Why would it be on the second one?
b. Putting a debugger just above that line I can see the element is present in the DOM and even more
if I release the debuger, same and silently does nothing but if I step over and run that line it actually scrolls.Edit: I attach a screenshot of the debugger, if I press f8 nothing, if I press f10 it scrolls
I would like to thin there's a cleaner way of making this work but can't find it
Thanks

React stackable snackbars/toasts

I'm creating my own simple snackbar/toast stacker. However, I'm having problems with queing them in an orderly manner. Removing a snackbar from the snackbar que causes re-render and odd behavior.
The basic flow:
Click a button which causes the addSnack function to fire which is provided by the withSnackbar HOC.
Take the parameters from the fired function, and create a snack accordingly and add it to the snackbar list.
At the end, we render the snackbar list.
Each snackbar controls it's own appearance and disappearance, and is controlled by a time out. After the timeout is fired, it calls removeSnack function which is suppose to remove the first snack from the list.
codesandbox
If you click the button for example, four times in a short amount of time. They render nicely, but when the first one is to be deleted, they all disappear and reappear abnormally.
I understand that it's partially the state re-renderings fault, however, I'm not sure how to handle it in a way that the removal is handled gracefully without affecting the rendering of other snacks.
So, after many hours of trial and error, I found a solution that works so far. Moving and reading the snacks outside of the state helped with the bizarre rendering problems, and with it, I was able to create a message que which works well.
Working example
Codesandbox
If you look at splice document, you will notice that it's returning an array of deleted elements and not the initial array.
You can correct it by splicing then updating:
snacks.splice(-1, 1);
addSnacks(snacks);
However you are still going to have some weird behavior and you might need to use a keyed list to fix that.
i had the same issue and i saw your solution, but i was really trying to find out why it happens - here is why:
when u call a useState hook from an async function's callback, you should use the callback format of the hook to make sure that you are working with the latest value. example:
const [messages, setMessages] = useState([]);
const addMessage = ( message ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
return [ ...prevMessages, message ];
});
};
const removeMessage = ( index ) => {
setMessages( prevMessages => {//prevMessages will be the latest value of messages
let newMessages = [...prevMessages];
newMessages.splice( index, 1 );
return newMessages;
});
};

React - Old promise overwrites new result

I have a problem and I'm pretty sure I'm not the only one who ever had it... Although I tried to find a solution, I didin't really find something that fits my purpose.
I won't post much code, since its not really a code problem, but more a logic problem.
Imagine I have the following hook:
useEffect(() => {
fetchFromApi(props.match.params.id);
}, [props.match.params.id]);
Imagine the result of fetchFromApi is displayed in a simple table in the UI.
Now lets say the user clicks on an entity in the navigation, so the ID prop in the browser URL changes and the effect triggers, leading to an API call. Lets say the call with this specific ID takes 5 seconds.
During this 5 seconds, the user again clicks on an element in the navigation, so the hook triggers again. This time, the API call only takes 0,1 seconds. The result is immediatly displayed.
But the first call is still running. Once its finished, it overwrites the current result, what leads to wrong data being displayed in the wrong navigation section.
Is there a easy way to solve this? I know I can't cancel promises by default, but I also know that there are ways to achieve it...
Also, it could be possible that fetchFromApi is not a single API call, but instead multiple calls to multiple endpoints, so the whole thing could become really tricky...
Thanks for any help.
The solution to this is extremely simple, you just have to determine whether the response that you got was from the latest API call or not and only then except it. You can do it by storing a triggerTime in ref. If the API call has been triggered another time, the ref will store a different value, however the closure variable will hold the same previously set value and it mean that another API call has been triggered after this and so we don't need to accept the current result.
const timer = useRef(null);
useEffect(() => {
fetchFromApi(props.match.params.id, timer);
}, [props.match.params.id]);
function fetchFromApi(id, timer) {
timer.current = Date.now();
const triggerTime = timer.current;
fetch('path').then(() => {
if(timer.current == triggerTime) {
// process result here
// accept response and update state
}
})
}
Other ways to handle such scenarios to the cancel the previously pending API requests. IF you use Axios it provides you with cancelToken that you can use, and similarly you can cancel XMLHttpRequests too.

Resources