Faced problem with interval not being cleared in React - javascript

I was doing a project in React and what I wanted to do is to start the calculation of factorial of 1000 on start button and cancel the calculation on cancel button click. Thus, I used setInterval here is the whole code:
import React, { useState } from "react";
const Button = ({ title, countButton }) => {
const [result, setResult] = useState(0);
let interval;
const handleFactorial = (num) => {
let iteration = 1;
let value = 1;
interval = setInterval(function () {
value = value * iteration;
console.log(iteration++);
if (iteration === num) {
setResult(value);
console.log(result);
clearInterval(interval);
}
}, 0);
};
let cancelFactorial = () => {
clearInterval(interval);
};
return countButton ? (
<button onClick={() => handleFactorial(1000)}>{title}</button>
) : (
<button onClick={cancelFactorial}>{title}</button>
);
};
export default Button;
The problem is when I click on cancel button which is this one <button onClick={cancelFactorial}>{title}</button> but calculation keeps going. Thus I need your help

You should use a reference for that as if you log your interval value, you will notice that you re-assign its value on every render.
const Button = ({ title, countButton }) => {
const intervalRef = useRef();
const handleFactorial = (num) => {
intervalRef.current = setInterval(function () {...}, 0);
};
let cancelFactorial = () => {
clearInterval(intervalRef.current);
};
...
}

You need to use [useRef][1] to keep a reference to your interval.
// don't do that
// let interval;
// do this instead
const intervalRef = useRef();
interval.current = setInterval(function () { ... })
const cancelFactorial = () => {
clearInterval(interval.current);
};

Related

react-native state is read-only

Following Component in react-native:
import { useEffect, useState } from 'react'
let startValue = null // only using this to restart the counter from resetTimer() (other better approaches?)
export const NewTimer = () => {
const [seconds, setSeconds] = useState(startValue)
const formatedTime = () => {
return [pad(parseInt(seconds / 60)), pad(seconds % 60)].join(':')
}
useEffect(() => {
const timer = setInterval(() => setSeconds(++seconds), 1000) // I guess this line triggers the error
return () => {
clearInterval(timer)
}
}, [])
return formatedTime
}
const pad = (num) => {
return num.toString().length > 1 ? num : `0${num}`
}
export const resetTimer = () => {
startValue = 0
}
results in Uncaught Error: "seconds" is read-only
Can anyone point where the mistake is? Thx!
when you do ++seconds, you are attempting to mutate seconds this render, which isn't allowed. I would use the setState callback to get the current value, and just do seconds + 1, which will accomplish the same thing:
useEffect(() => {
const timer = setInterval(() => setSeconds((seconds) => seconds + 1), 1000)
return () => {
clearInterval(timer)
}
}, [])

How to use setInterval with react useEffect hook correctly?

I tried to create a simple timer app with ReactJS and found the below code on the internet.
Does the function that we passed to the useEffect will execute with the dependency change or does it recreates with every dependency change and then execute?
Also I console log the return function of the useEffect and it runs with every render. Does it run only when the component unmount? or with every render?
import { useEffect, useState } from "react";
const App = () => {
const [isActive, setIsActive] = React.useState(false);
const [isPaused, setIsPaused] = React.useState(true);
const [time, setTime] = React.useState(0);
React.useEffect(() => {
let interval = null;
if (isActive && isPaused === false) {
interval = setInterval(() => {
setTime((time) => time + 10);
}, 10);
} else {
clearInterval(interval);
}
return () => {
console.log("cleanup");
clearInterval(interval);
};
}, [isActive, isPaused]);
const handleStart = () => {
setIsActive(true);
setIsPaused(false);
};
const handlePauseResume = () => {
setIsPaused(!isPaused);
};
const handleReset = () => {
setIsActive(false);
setTime(0);
};
return (
<div className="stop-watch">
{time}
<button onClick={handleStart}>start</button>
<button onClick={handlePauseResume}>pause</button>
<button onClick={handleReset}>clear</button>
</div>
);
};
export default App;
The code inside the useEffect hook will run every time a dependency value has been changed. In your case whenever isActive or isPaused changes state.
This means that the reference to the interval will be lost, as the interval variable is redefined.
To keep a steady reference, use the useRef hook to have the reference persist throughout state changes.
const App = () => {
const [isActive, setIsActive] = useState(false);
const [isPaused, setIsPaused] = useState(true);
const [time, setTime] = useState(0);
const interval = useRef(null)
useEffect(() => {
if (isActive && !isPaused) {
interval.current = setInterval(() => {
setTime((time) => time + 10);
}, 10);
} else {
clearInterval(interval.current);
interval.current = null;
}
return () => {
clearInterval(interval.current);
};
}, [isActive, isPaused])
...
}

How does setInterval work in reactjs (function components)?

I have got some states. I have got one button, which starts setInterval 1 second tick interval. Inside this setInterval I set these states.
let startGame;
const Cat = () => {
const [name, setName] = useState(null);
let [foodLevel, setFoodLevel] = useState(10);
let [healthLevel, setHealthLevel] = useState(10);
const [warning, setWarning] = useState('');
const [intervalId, setIntervalId] = useState(0);
const [value, setValue] = useState('');
const onChangeHandler = (event) => {
const { value } = event.target;
setValue(value);
};
const checkZeroFoodLevel = useCallback(() => {
return foodLevel <= 0;
}, [foodLevel]);
const tick = () => {
startGame = setInterval(() => {
if (!checkZeroFoodLevel()) {
setFoodLevel((prevFoodLevel) => prevFoodLevel - 1);
console.log(foodLevel); //foodLevel=10 every tick
} else {
setHealthLevel((prevHealthLevel) => prevHealthLevel - 1);
console.log(healthLevel);//healthLevel=10 every tick
}
}, 1000);
setIntervalId(startGame);
};
const startClickHandler = () => {
setName(value);
tick();
};
if (healthLevel === 0) {
clearInterval(intervalId);
}
return(...)
};
But when I click on this button, and output in a console my foodLevel state inside setInterval callback, this state does not update, but in a render is work fine. And when I output that from outside, it is updating as expect.
I found example with useRef():
let startGame;
const Cat = () => {
const [name, setName] = useState(null);
let [foodLevel, setFoodLevel] = useState(10);
let [healthLevel, setHealthLevel] = useState(10);
const currentFood = useRef(foodLevel);
currentFood.current = foodLevel;
const [warning, setWarning] = useState('');
const [intervalId, setIntervalId] = useState(0);
const [value, setValue] = useState('');
const onChangeHandler = (event) => {
const { value } = event.target;
setValue(value);
};
const checkZeroFoodLevel = useCallback(() => {
return currentFood.current <= 0;
}, [currentFood]);
const tick = () => {
startGame = setInterval(() => {
if (!checkZeroFoodLevel()) {
setFoodLevel((prevFoodLevel) => prevFoodLevel - 1);
console.log(currentFood);
} else {
setHealthLevel((prevHealthLevel) => prevHealthLevel - 1);
console.log(healthLevel);
}
}, 1000);
setIntervalId(startGame);
};
const startClickHandler = () => {
setName(value);
tick();
};
if (healthLevel === 0) {
clearInterval(intervalId);
}
return (...);
}
This is working fine. But I think that it is overhead. If I will have many states, I must create ref for each of them. I think, that it is not correct.
Can somebody explain to me why inside the function setInterval a state does not update? Can I do it without using useRef?

how to clearInterval in react hook on clicking button

I am building a simple timer with react hooks. I have two buttons start and reset.
handleStart function works fine when I click start button, the timer starts, but I can't figure out how to reset the timer on clicking the reset button.
Here is my code
const App = () => {
const [timer, setTimer] = useState(0)
const handleStart = () => {
let increment = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handleReset = () => {
clearInterval(increment) // increment is undefined
setTimer(0)
}
return (
<div className="App">
<p>Timer: {timer}</p>
<button onClick={handleStart}>Start</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}
In order to stop or reset the timer, I need to pass a property in clearInterval method. increment is defined in handleStart function so I can't access it in handleReset function. What to do?
You can set the timerId in ref and use it in your handleReset function. At present, the increment value is undefined for you because you have declared it within the handleStart function and hence hte scopre of the variable if limited to this function.
Also you can't define it directly inside App component as a variable since it will get reset when the App component re-renders. This is where ref comes in handy.
Below is a sample implementation
const App = () => {
const [timer, setTimer] = useState(0)
const increment = useRef(null);
const handleStart = () => {
increment.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handleReset = () => {
clearInterval(increment.current);
setTimer(0);
}
return (
<div className="App">
<p>Timer: {timer}</p>
<button onClick={handleStart}>Start</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}
Why not use hooks feature directly?
Define a interval state as you have defined timer state.
const [intervalval, setIntervalval] = useState()
Now you in handleStart set the state and in clearinterval you will have access to the modified state.
const handleStart = () => {
let increment = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000);
setIntervalval(increment);
}
const handleReset = () => {
clearInterval(intervalval);
setTimer(0);
}

Unable to access counter after gets updated

I want to start an interval by clicking on a button.
Here the interval gets started but, I can't access the value of counter. because when counter gets equal to 5, the interval should be stoped.
Here is the example:
let interval = null;
const stateReducer = (state, value) => value;
function App(props) {
const [counter, setCounter] = useReducer(stateReducer, 0);
const increment = () => {
interval = setInterval(() => {
setCounter(counter + 1);
if (counter === 5) clearInterval(interval);
console.log(counter);
}, 1000);
};
return (
<div>
<p>{counter}</p>
<button className="App" onClick={increment}>
Increment
</button>
</div>
);
}
You can run this code on codesandbox
change const stateReducer = (state, value) => value; to const
stateReducer = (state, value) => state+value;
make a variable let current_counter = 0; outside function
Change your increment function like this
current_counter = counter;
const increment = () => {
interval = setInterval(() => {
setCounter(1);
if (current_counter === 5) clearInterval(interval);
console.log(current_counter);
}, 1000);
};
Done
import React, { useReducer, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let interval = null;
let current_counter = 0;
const stateReducer = (state, value) => value;
function App(props) {
const [status, setStatus] = useState(false);
const [counter, setCounter] = useReducer(stateReducer, 0);
useEffect(()=>{
if(status){
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000)
return ()=>{
clearInterval(interval)
};
}
})
const increment = () => {
setStatus(!status)
};
return (
<div>
<p>{counter}</p>
<button className="App" onClick={increment}>
Increment
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Your code has a few problems.
Your reducer declaration
const initialState = { value : 0 }
const reducer = (state, action) =>{
if(action.type === 'INCREMENT') return { value : state.value + 1 }
return state
}
How you're setting your reducer
const [state, dispatch] = useReducer(reducer, initialState)
How you're dispatching your action
intervals are imperative code, you can't consistently declare an interval inside a React's handler without worrying about closure. You could use the click only to flag that the interval should start and handle all imperative code inside an useEffect. Here is a working example
const initialState = { value: 0 };
const reducer = (state, action) => {
if (action.type === "INCREMENT")
return {
value: state.value + 1
};
};
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
const [clicked, setClicked] = React.useState(false);
useEffect(() => {
let interval = null;
if (clicked) {
interval = setInterval(() => {
dispatch({ type: "INCREMENT" });
}, 1000);
}
if (state.value > 4) clearInterval(interval);
return () => clearInterval(interval);
}, [clicked, state]);
return <button onClick={() => setClicked(true)}>{state.value}</button>;
}
If your curious about closures and how react handle imperative code take a look on this awesome article from Dan Abramov (the most detailed explanation about effects out there).

Categories

Resources