Infinity state&component updating - javascript

I tried to create simple timer with useEffect and setInterval. In theory, on click it has to:
restart cooldown
const restartCooldown = () => {
setCooldownDuration(targetActivity['duration'])
}
useEffect() will see this and decrease cooldown every second within interval
useEffect(() => {
let interval
if (props.activity.cooldown) {
if (cooldownDuration > 0) {
if (props.activity.id === 1) {
console.log('Activity.js state', cooldownDuration)
}
interval = setInterval(() => setCooldownDuration(cooldownDuration - 1000), 1000);
} else {
clearInterval(interval)
}
} else {
setCooldownDuration(0)
}
});
useEffect() in ActivityProgression.js will get percentage of completion
useEffect(() => {
const interval = setInterval(() => setPercentages((props.totalTime - props.cooldown) / props.totalTime * 100), 1000);
})
4. Then it will be rendered
return <ProgressBar animated now={percentages} label={`${percentages}%`} />;
But in fact it doesn't work properly, creating infinity multiple renders on click.
Can you please tell me why? Much much thanks.
Full code repository to try it yourself
I tried everything, but it seems that i simply don't understand some react features :(

The second argument of useEffect is an array of dependencies. Since you didn't provide any dependencies, it runs every render. Because it updates the state, it causes a re-render and thus runs again, infinitely. Add [cooldownDuration] to your use effect like this:
useEffect(() => {...}, [cooldownDuration])
This will make the useEffect only run when cooldownDuration changes.

Related

React, can't access updated value of state variable inside function passed to setInterval() in useEffect()

I am building a simple clock app with React. Currently the countDown() function works, but I would like the user to be able to stop/start the clock by pressing a button. I have a state boolean called paused that is inverted when the user clicks a button. The trouble is that after the value of paused is inverted, the reference to paused inside the countDown() function passed to setInterval() seems to be accessing the default value of paused, instead of the updated value.
function Clock(){
const [sec, setSecs] = useState(sessionLength * 60);
const [paused, setPaused] = useState(false);
const playPause = () => {
setPaused(paused => !paused);
};
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
const interval = setInterval(() => {
countDown();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
I'm assuming it has something to do with the asynchronous nature of calls to setState() in React, and/or the nature of scoping/context when using regular expressions. However I haven't been able to determine what is going on by reading documentation related to these concepts.
I can think of some workarounds that would allow my app to function as desired. However I want to understand what is wrong with my current approach. I would appreciate any light anyone can shed on this!
In your code, the useEffect is called only once when mounting the component.
The countdown function registered inside will have its initial value at the time when the useEffect/setInterval is called. So paused will only have the value when you initially mount the component. Because you are not calling countDown directly or updating its value inside your useEffect, it is not updated.
I think you could solve this issue like this:
interval.current = useRef(null);
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
clearInterval(interval.current);
interval.current = setInterval(countDown, 1000);
return () => {
clearInterval(interval.current);
};
}, [paused]);
your useEffect is dependent on the value of paused as it needs to create a new interval (with a different countdown function). This will trigger the useEffect not only on mount but every time paused changes. So one solution would be to clear the interval and start a new one with a different callback function.
Edit: You could actually improve it as you only want the interval to be running if the countDown function actually does something so this should work too:
useEffect(() => {
clearInterval(interval.current);
if(!paused) {
interval.current = setInterval(countDown, 1000);
}
return () => {
clearInterval(interval.current);
};
}, [paused]);

Having some troubles getting useEffect, useCallback and setTimeout to work together

Trying to implement some custom scroll behaviour for a slider based on the width. It's using useState, to keep track of the current and total pages inside the slider. This is the code I have ended up with trying to acomplise it but it has some unexpected behaviour.
const [isTimeoutLocked, setIsTimeoutLocked] = useState(false)
const handleScroll = useCallback(() => {
const gridContainer = document.querySelector(".grid");
const totalPages = Math.ceil(
gridContainer.scrollWidth / gridContainer.clientWidth + -0.1
);
setTotalPages(totalPages);
const scrollPos = gridContainer.clientWidth + gridContainer.scrollLeft + 2;
if (gridContainer.scrollWidth > scrollPos) {
gridContainer.scrollBy({
left: gridContainer.clientWidth + 20.5,
behavior: "auto",
block: "center",
inline: "center",
});
setCurrentPage(currentPage + 1);
setIsTimeoutLocked(false)
} else {
gridContainer.scrollTo(0, 0);
setCurrentPage(1);
setIsTimeoutLocked(false)
}
},[currentPage]);
useEffect(() => {
if(!isTimeoutLocked){
setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
}
}, [currentPage, displayDate, isTimeoutLocked, handleScroll]);
The problem here is that when the currentPage is displayed in the ui it will reset back to one if the else runs inside the handleScroll function which is totally fine but then it will result back to the previous value instead of going back to 2 when it gets to the next page. Also since I have added the isTimeoutLocked as a dependency to the useEffect since it was asking for it the setTimeout will run more often but I only want to have it as a dependency to get the correct value not so the useEffect runs everytime it changes. The purpose of the isTimeoutLocked is so when you change the content inside the container by changing the current day it has not registered a bunch of timeouts.
You should add a cleanup function to your useEffect. Since you mutate isTimeoutLocked in your useEffect it can force multiple setTimeouts to run and that is probably the result of wierd behaviour.
When using setTimeout inside useEffect it is always recommended to use it with cleanup. Cleanup will run before the next useEffect is triggered, that gives you a chance to bail out of next setTimeout triggering. Code for it is this:
useEffect(() => {
if(!isTimeoutLocked){
const tid = setTimeout(() => {
setIsTimeoutLocked(true)
document.querySelector(".grid") && handleScroll();
}, 5000);
// this is the cleanup function that is run before next useEffect
return () => clearTimeout(tid);
}
You can find more information here: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup .
Another thing to be careful is the async nature of setState and batching. Since you are using setState inside a setTimeout, React will NOT BATCH them, so EVERY setState inside your handleScroll will cause a rerender (if it was not inside a setTimeout/async function React would batch them). Since you have alot of interconected states you should look into useReducer.

How to trigger a function at regular intervals of time using hooks and when certain criteria is met I want to clear the time interval?

I have a react component that performs a certain task at regular intervals of time after mounting. But I want to clear the interval once after a criterion is met. How can I achieve that?
My code
const [totalTime, setTotalTime] = React.useState(10000);
const foo = () => {
console.log("Here");
};
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
});
React.useEffect(() => {
let originalInterval;
if (totalTime > 0)
originalInterval = setInterval(() => {
foo();
console.log(totalTime);
}, 5000);
return () => clearInterval(originalInterval);
}, []);
When I watch the console even after 10000ms It is still logging Here and also totalTime is always being 10000ms. I am not able to figure out what exactly is happening.
You may need to pass the older state as an argument to the setTotalTime updater function. You also may need to pass (another) state variable as a dependency to the useEffect hook so that the function is executed every time the state variable changes
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime => totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
}, [...]);
Depends on your criteria, and what you call a criteria, but you could just use another state and then useEffect on that another state:
const [criteriaIsMet,setCriteriaIsMet] = useState(false);
useEffect(() => { if(criteriaIsMet) {clearInterval(...)} },[criteriaIsMet])
And then somewhere when you do your actual "criteria logic" you just go ahead and do setCriteriaIsMet(true)
And how would you know Id of interval in above useEffect - again you could just create a special state that will store that id, and you could set it in your original useEffect
As for why your current code is not clearing the interval is because when you use useEffect with empty array as second argument, and return function in first function argument - that will be execute on component unmounting
And these are just one of numerous options you have actually :)

Why useEffect cleanup logging every time?

I'm implementing count down
and use useRef hook to using it when clean setTimeout when the user navigates to the next screen to avoid cancel all subscription warning and it's work!
But I have something weird when count - 1 i can see "hey" in the console! although not cleaning the setTimeOut!!
I don't want to clean it in this case but why should loggin every time count changes!
code snippet
const [seconds, setSeconds] = useState(40);
const countRef = useRef(seconds);
useEffect(() => {
if (seconds > 0) {
countRef.current = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
} else {
setSeconds(0);
}
return () => {
console.log('hey'); // every count down it's appeared
clearTimeout(countRef.current);
};
}, [seconds]);
You see "hey" because you're using seconds as a dependency. So every time seconds changes, the effect must run again leading to the effect's destroy function (the function you returned from the effect) to be invoked.
Instead of having seconds as a dependency, you should instead have setSeconds.
const [seconds, setSeconds] = React.useState(10);
useEffect(() => {
let didUnsub = false;
const id = setInterval(() => {
setSeconds((prev) => {
// while the count is greater than 0, continue to countdown
if (prev > 0) {
return prev - 1;
}
// once count eq 0, stop counting down
clearInterval(id);
didUnsub = true;
return 0;
});
}, 1000);
return () => {
console.log("unmounting");
// if the count didn't unsubscribe by reaching 0, clear the interval
if (!didUnsub) {
console.log("unsubscribing");
clearInterval(id);
}
};
}, [setSeconds]);
If you look at the example below, you'll see that the effect is only run once, when the component is mounted. If you were to cause the component to dismount, the destroy function would be invoked. This is because the setState is a dispatch function and doesn't change between renders, therefor it doesn't cause the effect to continuously be called.
In the example you can click the button to toggle between mounting and dismounting the counter. When you dismount it notice that it logs in the console.
Example: https://codesandbox.io/s/gallant-silence-ui0pv?file=/src/Countdown.js

React useEffect Warning: Maximum update depth exceeded. Is there a way around this?

As shown in the code below, I am using the useEffect hook to watch for changes to a percentage variable, then set a timer to increment that variable in 1 second. The callback will occur as soon as the page loads, kicking the process off.
The percentage variable is used to render a line chart that increments until a passed value is reached. (It basically shows a % pass rate, but on load the bars will move across the screen up to their %.)
Everything works fine, but the console flags the maximum update depth warning which points out it's trying to prevent an infinite loop when i'm using setState inside useEffect. I get the point here, but I know I'll never pass in a value above 100 and so there will never be an infinite loop. I also need the recursive functionality to have my charts appear to populate dynamically.
Even if I decided to ignore the warning, it's creating error in my testing if I mount the component.
Is there a way I can tell react that this won't be an infinite loop and get the error to go away? Or do I need a different approach?
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
let timer = null;
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer);
return;
}
timer = setTimeout(setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;
Thanks for any help
In timer = setTimeout(setPercent(newPercent), 1000);
setPercent(newPercent) will be called immediately. setTimeout requires a function (instead of a function call):
timer = setTimeout(() => setPercent(newPercent), 1000);
Another thing to keep in mind is that timer will be defined on each render, thus clearTimeout won't have any effect. To avoid this, you can create a ref so that the value will be remembered. Like so:
const timer = React.useRef()
using/setting the value is done to the current property
timer.current = ...
Working example:
const ReportProgressSummary = ({result, title}) => {
const [percent, setPercent] = useState(0);
const timer = React.useRef();
useEffect( () => {
let newPercent = percent + 1;
if (newPercent > result) {
clearTimeout(timer.current);
return;
}
timer.current = setTimeout(() => setPercent(newPercent), 1000);
}, [percent]);
return (
<ContainerStyled>
<ChartContainerStyled>
<ChartTitleStyled>{title}</ChartTitleStyled>
<CheckboxStyled type="checkbox" />
</ChartContainerStyled>
<ChartContainerStyled>
<LineContainerStyled>
<Line trailWidth="2" trailColor="red" strokeWidth="4" strokeColor="green" percent={percent}/>
</LineContainerStyled>
<h6>{percent}%</h6>
</ChartContainerStyled>
</ContainerStyled>
)
};
export default ReportProgressSummary;

Categories