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

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)
}

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();
}

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

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])

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!

Ensuring React state has updated for game loop

I am writing a version of Conway's Game of Life in React. The component's state contains the grid describing which of the cells is alive at the current time. In each game loop, the new grid is calculated and the state is updated with the next iteration.
It occurs to me that since setState is asynchronous, when repeatedly calling the iterate function with setInterval, I am not guaranteed to be using the current version of grid each time iterate runs.
Is there an alternative to using setInterval in React that would avoid any potential issues caused by setState being asynchronous?
Here are the relevant functions that describe the game loop:
go = () => {
const { tickInterval } = this.state;
this.timerId = setInterval(this.iterate, 570 - tickInterval);
this.setState({
running: true,
});
};
iterate = () => {
const { grid, gridSize, ticks } = this.state;
const nextGrid = getNextIteration(grid, gridSize);
this.setState({
grid: nextGrid,
ticks: ticks + 1,
});
};
If you need to set state based on a current state, it is wrong to directly rely on this.state, because it may be updated asynchronously. What you need to do is to pass a function to setState instead of an object:
this.setState((state, props) => ({
// updated state
}));
And in your case it would be something like:
iterate = () => {
this.setState(state => {
const { grid, gridSize, ticks } = state;
const nextGrid = getNextIteration(grid, gridSize);
return {
grid: nextGrid,
ticks: ticks + 1
}
});
};
SetState is Asynchronous
this.setState({
running: true,
});
To make it synchronously execute a method:
this.setState({
value: true
}, function() {
this.functionCall()
})
If you have a look at the react official documentation, the setState api does take a callback in following format:
setState(updater[, callback])
Here the first argument will be your modified state object and second argument would be callback function to be executed when setState has completed execution.
As per the official docs:
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
You can have a look at official docs to get more information on this.

setInterval function is running in other components also in Angular 6

I am new to Angular(6). I am using setInterval function in a component. It is working but when I navigate to another route, setInterval continues to execute. Please help me to identify the reason.
//Calling it in ngOnit()
autosavedraftsolution() {
setInterval(() => {
console.log(this.draftSolutionForm);
if (this.solutionTitleValid) {
this.savedraftsolution();
}
}, this.autoSaveInterval);
}
//savedraftsolution()
savedraftsolution() {
console.log("saving..");
this.connectService.saveDraftSolution({
Title: this.draftSolutionForm.get('Title').value,
Product: this.draftSolutionForm.get('Product').value
} as Draftsolution).subscribe(draftsol => {
console.log("saved");
});
}
It keeps on showing me "saving.." and "saved" message in console.
You need to call clearInterval to stop it when your component unmounts:
this.intervalId = setInterval(...);
When your component is unmounting:
ngOnDestroy() {
clearInterval(this.intervalId);
}
Dominic is right. You have to clear the interval when the component is destroyed. You can make something like this
ngOnInit(){
this.saveInterval = setInterval(() => {}, 1000)
}
ngOnDestroy(){
clearInterval(this.saveInterval)
}
Make sure that your component implements OnInit and OnDestroy.

Categories

Resources