React: Setinterval is not stopping even after clearing the timer using clearInterval - javascript

I have made a basic slideshow demo where on checking the checkbox slideshow is enabled. Problem is that once enabled slide show can't be disabled even if I uncheck the checkbox. As per muy understanding, I'm learning the timer and also nullifying the state storing the timer but the slide show keep on going.
Specifically this part gets invoked on checkbox update:
useEffect(() => {
if (isTrue) {
setSlideTimer(() => {
return setInterval(() => {
forwardButton.current.click();
}, slideDuration);
});
} else {
clearInterval(slideTimer);
setSlideTimer(null);
}
}, [isTrue]);
From browser logs it is evident that timer indeed got cleared. Though there is a warning "... component is changing an uncontrolled input of type checkbox to be controlled" but I'm not sure if that's the culprit here.

Issue
The issue is that you've a missing dependency on the sliderTimer state.
useEffect(() => {
if (isTrue) {
setSlideTimer(() => {
return setInterval(() => {
forwardButton.current.click();
}, slideDuration);
});
} else {
clearInterval(slideTimer);
setSlideTimer(null);
}
}, [isTrue]);
Solution
Don't generally store timer ids in state. Use a React ref is you need to access the timer id outside the useEffect hook, otherwise just cache it locally within the useEffect hook's callback. In this case you will want to use a ref to hold the sliderTimer id value so it can also be cleared in the case the component unmounts.
Example:
const sliderTimerRef = React.useRef();
useEffect(() => {
// Clear any running intervals on component unmount
return () => clearInterval(sliderTimerRef.current);
}, []);
useEffect(() => {
if (isTrue && forwardButton.current) {
sliderTimerRef.current = setInterval(
forwardButton.current.click,
slideDuration
);
} else {
clearInterval(sliderTimerRef.current);
}
}, [isTrue]);
Additional issue
From browser logs it is evident that timer indeed got cleared. Though
there is a warning "... component is changing an uncontrolled input of type checkbox to be controlled" but I'm not sure if that's the
culprit here.
This is typically the case when the value or checked prop changes from an undefined to a defined value. Ensure whatever the checked state is that it is initially defined, even if just false.

Try this!
useEffect(() => {
let interval;
if (isChecked) {
interval = setInterval(() => {
forwardButton.current.click();
}, 1000); // change it
}
return function cleanup() {
clearInterval(interval);
console.log('Clear timer');
};
}, [isChecked]);

Another approach is to use the setTimeout() method. For this you would need to create a new state clicks (or any other name) that will trigger the useEffect every time it changes, allowing setTimeout to work as setInterval
const [clicks, setClicks] = useState(0)
useEffect(() => {
if(isChecked){
setTimeout(() => {
forwardButton.current.click()
setClicks(clicks + 1)
}, slideDuration)
}
}, [isChecked, clicks])

Related

How to call setState without any triggering events?

I want to update the value of a counter variable after some fixed time indefinitely. Here is my code:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
myCount: 1
};
}
// Update state after every 15 seconds.
setTimeout(function(){
this.setState({myCount: this.state.myCount + 1});
}, 15000);
}
However, I get error about unexpected token with this component. How can I set state within the class properly without using any event listeners?
Thanks.
Your call to setTimeout is at the top level of the class body where properties, the constructor, and methods are expected. You can't put arbitrary code there. (Though you could could have a property initializer; not a good idea in this case, though.)
Put it in componentDidMount; and be sure to saved the timer handle (on the instance, usually) and clear the timer in componentWillUnmount so it doesn't fire if the component is unmounted before the timer callback occurs:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
myCount: 1,
};
}
componentDidMount() {
// Update state after every 15 seconds.
this.timerHandle = setTimeout(() => { // ****
this.setState((previousState) => { // ****
return { myCount: previousState.myCount + 1 }; // ****
}); // ****
}, 15000);
}
componentWillUnmount() {
clearTimeout(this.timerHandle);
}
}
Side note: Notice the changes on the **** lines above
I replaced your traditional function with an arrow function so it closes over this.
I changed your callback to use the callback version of setState, which is generally best when setting state based on existing state.
...after some fixed time indefinitely.
A setTimeout callback will only occur once. You may want setInterval if you want the counter updated every 15 seconds. Or alternatively, start a new setTimeout when the callback runs, like this:
startCounterTimer() {
// Update state after every 15 seconds.
this.timerHandle = setTimeout(() => {
this.setState((previousState) => {
this.startCounterTimer(); // ***
return { myCount: previousState.myCount + 1 };
});
}, 15000);
}
componentDidMount() {
this.startCounterTimer();
}

why is my gloval setInterval ID variable unavailable to a function?

I am writing a simple timer app and using setInterval for the first time. The project is in react typescript and I use the useReducer hook to manage state.
The project requires two separate timers, session and break, which may explain some of the code. The project also requires one button to start and stop the timer, hence I am using a single function.
I have redacted my reducer and types as the problem, as i understand, does not involve them, but some simple use of setInterval that I just don't get yet.
When the startStopTimer function is called a second time, the intervalID is undefined, even though it is declared globally.
const App = () => {
const [timerState, dispatch] = useReducer(timerStateReducer, initialTimerState
);
let intervalID: NodeJS.Timer;
const startStopTimer = () => {
let name: string = timerState.session.count !== 0 ? 'session' : 'break';
if (timerState[name].running) {
console.log('stopping timer' + intervalID);
//reducer sets running boolean variable to false
dispatch({type: ActionKind.Stop, name: name});
clearInterval(intervalID);
} else {
//reducer sets running boolean variable to true
dispatch({type: ActionKind.Start, name: name});
intervalID = setInterval(() => {
dispatch({type: ActionKind.Decrease, name: name});
}, 1000);
}
};
return (
//redacted JSX code
<button onClick={startStopTimer}>Start/Stop</button>
)
I have tried passing onClick as an arrow function (rather than a reference, i think?) and it behaves the same. I tried simplifying this with useState, but came across a whole 'nother set of issues with useState and setInterval so I went back to the useReducer hook.
Thanks!

React - Passing value to setState callback

I'm trying to pass a value determined outside my prevState callback into the function. Here is what I have:
uncheckAllUnsentHandler = (e) => {
const newCheckMarkValue = e.target.checked;
this.setState((prevState, props, newCheckMarkValue) => {
const newUnsentMail = prevState.unsentMail.map(policy => {
mail.willSend = newCheckMarkValue;
return mail;
});
return {
unsentMail: newUnsentMail
}
});
}
newCheckMarkValue is undefined inside the map function and I'm not sure why.
Long description:
I have a table where clients select whether they want to send mail or not. By default all mail items are checked in the table. In the header they have the ability to uncheck/check all. I'm trying to adjust the state of the mail items in the table to be unchecked (willSend is the property for that on Mail) when the header checkbox is clicked to uncheck. All of this works if I hardcode the willSend to true or false in code (ex: mail.willSend = true;) in the code below, but I can't seem to get the value of the of the checkbox in the header in.
setState
The updater function takes only two arguments, state and props. state is a reference to the component state at the time the change is being applied.
(state, props) => stateChange
You can simply access the version of newCheckMarkValue enclosed in the outer scope of uncheckAllUnsentHandler.
uncheckAllUnsentHandler = (e) => {
const newCheckMarkValue = e.target.checked;
this.setState((prevState, props) => {
const newUnsentMail = prevState.unsentMail.map(policy => {
mail.willSend = newCheckMarkValue;
return mail;
});
return {
unsentMail: newUnsentMail
}
});
}

Is there a way to memoize a function passed from params - (useCallback) exhaustive-deps

So I have this little snippet:
const useTest = (callbackFunc) => {
const user = useSelector(selector.getUser); // a value from store
useEffect(() => {
if (user.id === 1) {
callbackFunc()
}
}, [callbackFunc, user.id])
}
On the code above if callbackFunc is not memoized (wrapped in useCallback) before passing, the useEffect will trigger an infinite loop.
Wrapping the function on a useCallback inside the hook will still trigger an infinite loop, so that's a no:
const cb = () => useCallback(() => callbackFunc, [callbackFunc]);
The above will trigger infite loop because callbackFunc.
YES I am well aware that I can just wrap the function inside useCallback before passing to this hook, my only problem with that is: there will be a high probability that other/future devs will just pass a non-memoized function when calling this hook and this is what worries me. `
I can't remove the callbackFunc on the useEffect/useCallback second parameter array because of exhaustive-deps rule - and removing it (or on this line) is also prohibited by the higher devs.
Any idea how can I possibly achieve my goal? If none thatn I'll just pray that other devs will read the hook first before using it.
You can do something like this, but you won't be able to modify the callback anymore
const funcRef = React.useRef(null)
useEffect(() => {
funcRef = callbackFunc
}, [])
useEffect(() => {
if (funcRef.current){
if (user.id === 1) {
funcRef.current()
}
}
}, [funcRef, user.id])

React mounted component setInterval is not clearing interval due to "state update"

I have an interval timer set for a function that sets today's date into the state that I initialize in componenteDidMount. Although I clear the interval in the componentWillUnmount, it still ends up giving an error after quickly switching between components (which is how I caught the bug).
This is the error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
I have tried manipulating a private _isMounted variable from false to true throughout the cycle and forcing a conditional check in my setTodaysDate() prior to setting state, but even that didn't solve the problem.
// _isMounted = false; <----- tried this method to no avail
state = {
selectedDate: ""
};
componentDidMount() {
this.setTodaysDate();
this.interval = setInterval(() => this.setTodaysDate(), 40 * 1000 * 360);
// this._isMounted = true; <----- tried
}
componentWillUnmount() {
clearInterval(this.interval);
// this._isMounted = false; <----- tried
}
setTodaysDate = () => {
// if (this._isMounted) { <----- tried
this.setState({
selectedDate: moment(moment(), "YYYY-MM-DDTHH:mm:ss")
.add(1, "days")
.format("YYYY-MM-DD")
});
// } <----- tried
}
I'm at a loss as to how else to "plug the leak."
Edit: It turns out, thanks to Gabriele below, the real cause was a lodash debounce method I was using (where I also setState) that I never cancelled during the unmount, leading to the "leak":
debounceCloseAlert = _.debounce(() => {
this.setState({ alertVisible: false });
}, 5000);
Looking at your component i do not think the issue is with your setInterval. The way you handle it is the correct approach and should not produce said error.
The problem i believe is with the use of _.debounce in your debounceCloseAlert method. It also will create a timeout and you are not clearing that anywhere.
The returned value from _.debounce includes a .cancel() method to clear the interval. So just call this.debounceCloseAlert.cancel(); in your componentWillUnmount and it will clear it.
Have you tried saving the interval reference in the component's state ?
state = {
selectedDate: "",
interval: null
};
componentDidMount() {
this.setTodaysDate();
const interval = setInterval(() => this.setTodaysDate(), 40 * 1000 * 360);
this.setState({interval});
}
componentWillUnmount() {
clearInterval(this.state.interval);
}
setTodaysDate = () => {
this.setState({
selectedDate: moment(moment(), "YYYY-MM-DDTHH:mm:ss")
.add(1, "days")
.format("YYYY-MM-DD")
});
}
Some people also seem to have had some luck by using interval._id:
(Using your initial code)
componentWillUnmount() {
clearInterval(this.interval._id)
}

Categories

Resources