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();
}
Related
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])
In my application, I'm using a dispatch from useReducer hook on click of a button and in the same function I'm using a setTimeout of 2 seconds. But when I store the data using dispatch of usereducer then I'm not getting updated value in setTimeout function.
I cannot share original code, but sharing a snippet of another demo app where this issue occurs.
const initialData = { data: "ABC" };
function reducer(state = initialData, action) {
switch (action.type) {
case "STORE":
return {
...state,
data: action.payload
};
default:
return state;
break;
}
}
function Demo() {
const [state, dispatch] = React.useReducer(reducer, initialData);
console.log("Render : ",state.data); //Will be executed on each rendering
const handleClick = () => {
dispatch({
type: "STORE",
payload: state.data + parseInt(Math.random() * 10)
});
setTimeout(() => {
console.log("ButtonClick : ",state.data); //Will be executed after 2 seconds of dispatching.
}, 2000);
};
return <button onClick={handleClick}>{state.data}</button>;
}
ReactDOM.render(<Demo />, document.getElementById("app"));
In above example I'm storing data in reducer using dispatch, and I'm calling console.log("ButtonClick") on Button Click after 2 seconds but even after 2 seconds, I'm not getting updated data in console. But in console.log("Render") I'm getting updated data.
Live example on : https://codepen.io/aadi-git/pen/yLJLmNa
When you call
const handleClick = () => {
dispatch({
type: "STORE",
payload: state.data + parseInt(Math.random() * 10)
});
setTimeout(() => {
console.log("ButtonClick : ",state.data); //Will be executed after 2 seconds of dispatching.
}, 2000);
};
this is what happens:
Run dispatch with an object to store some data. This function is executed asynchronically, so the result is not available immediately.
Register a timeout handler, which logs the current value of state.data to the console. Since the preceding dispatch is still working in progress, the value of state.data is still the old one.
This means you can not log a new dispatched value by running console.log after your dispatch call because you can not see into the future. You can only log the new data after a re-render of your component due to changing state. Then you can and should use
React.useEffect(() => {
console.log(state.data);
}, [state.data]);
Some more explanations about setTimeout and why console.log logs old values within it
You use
setTimeout(() => {
console.log("ButtonClick : ", state.data);
}, 2000);
and this is equivalent to
const callback = () => console.log("ButtonClick : ", state.data);
setTimeout(callback, 2000);
In the first line you create a function (named callback here), which prints some text. This text consists of a simple string and the value of state.data. Therefore this function has a reference to the variable state.data. The function in combination with the reference to the outer state is called closure and this closure ensures, that the value state.data is kept alive as long as the function lives (is not binned by the garbage collector).
In the second line setTimeout is called with this function. Simplified this means a task is stored somewhere which states, that exactly this function has to be executed after the given timeout. So as long as this task is not done, the function stays alive and with it the variable state.data.
In the meantime long before the task is handled, the new action is dispatched, new state calculated and Demo re-rendered. With that a new state.data and a new handleClick is created. With the new creation of handleClick also a new function is created which is passed to setTimeout. This whole handleClick function is then passed as onClick handler for the button element.
The re-render is now over, but the task from before is still pending. Now when the timeout duration ends (long after re-rendering the component) the task is taken from the task queue and executed. The task still has reference to the function from the render before and this function still has reference to the value state.data from the render before. So the value logged to the console is still the old value from the render before.
I have a service which gets returns some data at the page load,
getAllTurbinesStat(){
return this.http.get(this.url_stat_Turbines_all);
}
in my component I consume this service:
this.service.getAllTurbinesStat().subscribe( s => {
this.allStats.push(s);
});
allStat is an array, now every 3 minutes this function should be run to update data, should setInterval be in service or in my component? And how should I write it? Because the first time I don't need the setinterval, because once the page loaded first time, my data is updated.
You can try this.
First create one function in your component like this.
getAllTurbinesStat() {
this.service.getAllTurbinesStat().subscribe(s => {
this.allStats.push(s);
});
}
And then in from ngOnInit() or from constructor of your component use this.
this.getAllTurbinesStat();
setInterval(() => this.getAllTurbinesStat(), 180000);
You can use interval for that like below:
in you service
getAllTurbinesStat(){
return this.http.get(this.url_stat_Turbines_all);
}
getData(): {
return interval(3000).pipe(
switchMap( () => this.getAllTurbinesStat())
)
}
in your component
this.service.getData().subscribe( s => {
this.allStats.push(s);
});
You can use timer from rxjs.
import { timer } from 'rxjs';
/*
timer takes a second argument, how often to emit subsequent values
in this case we will emit first value after 0 second and subsequent
values every 3 minutes after
*/
const source = timer(0, 180000);
//output: 0,1,2,3,4,5......
const subscribe = source.subscribe(val => console.log(val));
For you case.
return timer(0, 180000).pipe(
flatMap( () => this.getAllTurbinesStat())
)
First call your function one time in the ngOnInit of your component then get on with setInterval.
ngOnInit() {
getAllTurbinesStat();
setInterval(getAllTurbinesStat(), 3000);
}
getAllTurbinesStat() {
return this.http.get(this.url_stat_Turbines_all);
}
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)
}
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.