I'm trying to make a timer that starts at 1.0 and increases 0.1 until it reaches a random number. You have to start a "session" 5 seconds after the component is rendered and this is all working. The problem is that I want a session to start 5 seconds after another finishes and React ignores the setState that would allow those next sessions and i dont understand why.
Note: It only works if I create a button that calls a toggle function with
setIsGameActive(isGameActive => isGameActive = !isGameActive)
and click it
const GamePanel = () => {
const [gameTime, setGameTime] = useState(1);
const [isGameActive, setIsGameActive] = useState(false);
const [gameEndTime, setGameEndTime] = useState(0);
const [lastResults, setLastResults] = useState([]);
let randomNumber = 0;
const useFirstTime = useRef({firstTime: true});
const makeRandomNumber = (min, max) => {
return min + (max - min) * Math.random()
}
const reset = () => {
setGameTime(1);
setIsGameActive(false); //ignored
}
const gameStart = () => {
if(useFirstTime.current.firstTime === false){
if(!isGameActive){
console.log("Game starting...");
randomNumber = makeRandomNumber(1.1, 5);
setGameEndTime(randomNumber.toFixed(1))
setIsGameActive(true);
}
}
}
const gameFinished = () => {
setTimeout(()=>{
reset();
gameStart()
}, 5000)
}
useEffect(() => {
if(useFirstTime.current.firstTime === true){
useFirstTime.current.firstTime = false;
setTimeout(() => {
gameStart();
}, 5000);
}
let interval = null;
if(isGameActive){
interval = setInterval(() => {
if(gameTime.toFixed(1) !== gameEndTime){
setGameTime(gameTime => gameTime + 0.1)
}
if(gameTime.toFixed(1) === gameEndTime){
clearInterval(interval)
gameFinished();
}
}, 100);
}else if(!isGameActive && gameTime !== 0){
clearInterval(interval)
}
return () => clearInterval(interval);
}, [isGameActive, gameTime, gameEndTime, gameStart, gameFinished]);
return(
<section id="game-panel">
<h1></h1>
<h1>{gameTime.toFixed(1)}x</h1>
</section>
);
}
export default GamePanel;
Thanks!!
The reset function is created everytime your Component is rendered. Because of this the reset reference inside the timer is different than in the component itself after rerendering. Try to use useCallback for reset to create a memorized version of the function: https://reactjs.org/docs/hooks-reference.html#usecallback
const reset = useCallback(() => {
setGameTime(1);
setIsGameActive(false); //ignored
},[])
Related
Currently, I am trying to build a timer, but it stops after one second. I am trying to use hooks in React and I am trying to figure out how to implement useEffect into my startTimer function so it can countdown.
function InputTimer() {
const [timerOn, setTimerOn] = useState(false);
const [timerStart, setTimerStart] = useState(0);
const [timerTime, setTimerTime] = useState(0);
let timer;
const startTimer = () => {
setTimerOn(true);
setTimerStart(0);
setTimerTime(0);
timer = setInterval(() => {
const newTime = timerTime - 1;
if (newTime >= 0) {
setTimerTime(newTime);
} else {
clearInterval(timer);
setTimerOn(false);
alert("Countdown Ended");
}
}, 1000);
}
}
It appears your code is working.
However, you just set the time to be zero, so after a second, it exits.
To properly test, your code, set the start time to 10 seconds: setTimerTime(10);
Full Code
function InputTimer() {
const [timerOn, setTimerOn] = useState(false);
const [timerStart, setTimerStart] = useState(0);
const [timerTime, setTimerTime] = useState(0);
let timer;
const startTimer = () => {
setTimerOn(true);
setTimerStart(10); // Change this line
setTimerTime(0);
timer = setInterval(() => {
const newTime = timerTime - 1;
if (newTime >= 0) {
setTimerTime(newTime);
} else {
clearInterval(timer);
setTimerOn(false);
alert("Countdown Ended");
}
}, 1000);
}
}
You've closed over the timerTime value in the interval callback scope, it never changes.
Here's a minimal example of your logic that runs:
Split the logic up, use the interval callback to decrement the time and use a useEffect hook to check the termination condition. Use a React ref to store the timer reference.
function InputTimer() {
const [timerTime, setTimerTime] = useState(10);
const timerRef = useRef();
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
useEffect(() => {
if (timerTime <= 0) {
clearInterval(timerRef.current);
alert("Countdown Ended");
}
}, [timerTime]);
const startTimer = () => {
setTimerTime(10);
timerRef.current = setInterval(() => {
setTimerTime(time => time - 1);
}, 1000);
}
return (
<>
<div>Time: {timerTime}</div>
<button type="button" onClick={startTimer}>Start</button>
</>
)
}
I'm creating this countdown app where it should display a random word and then start the timer.
Timer works fine but the real problem is when I uncomment the line where the new word is updated in the state.
Since the state is changed, the component re-renders. Again the state is changed causing it to rerender and so on going into an infinite loop of updating the word. I'm not getting how to solve this in an optimal way.
EDIT: The new word generation process should repeat after timer hits zero.
function App() {
const [newWord, setNewWord] = React.useState('Default');
const [time, setTime] = React.useState(6);
React.useEffect(() => {
// setNewWord(generateWord())
let intervalId = setInterval(() => {
console.log('time reduced to ', time);
if (time > 0) {
setTime(time - 1);
}
if (time === 0) {
clearInterval(intervalId);
// exitGame();
}
}, 1000);
return () => {
clearInterval(intervalId);
};
})
// Generate a new word
function generateWord() {
let words = ['yeah', 'lol', 'gotcha']
let randIndex = Math.floor(Math.random() * words.length);
return words[randIndex];
}
return (
<div>
<div> Word = {newWord} </div>
<div> Time = {time} </div>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Just call generateWord and pass the result as the initial state:
function App() {
const [newWord, setNewWord] = React.useState(generateWord());
const [time, setTime] = React.useState(6);
React.useEffect(() => {
let intervalId = setInterval(() => {
console.log('time reduced to ', time);
if (time > 0) {
setTime(time - 1);
}
}, 1000);
return () => {
clearInterval(intervalId);
};
}, [time])
React.useEffect(() => {
if (time === 0) {
// exitGame(); ?
setNewWord(generateWord())
setTime(6)
}
}, [time])
// Generate a new word
function generateWord() {
let words = ['yeah', 'lol', 'gotcha']
let randIndex = Math.floor(Math.random() * words.length);
return words[randIndex];
}
return (
<div>
<div> Word = {newWord} </div>
<div> Time = {time} </div>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Set the work's useEffect() dependencies to an empty array, so it won't be called after initial re-render. Move the exitGame logic to another useEffect().
function App() {
const intervalId = React.useRef();
const [newWord, setNewWord] = React.useState('Default');
const [time, setTime] = React.useState(6);
React.useEffect(() => {
setNewWord(generateWord())
intervalId.current = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time);
}, 1000);
return () => {
clearInterval(intervalId.current);
};
}, [])
React.useEffect(() => {
console.log(time);
if (time === 0) {
clearInterval(intervalId.current);
// exitGame();
}
})
// Generate a new word
function generateWord() {
let words = ['yeah', 'lol', 'gotcha']
let randIndex = Math.floor(Math.random() * words.length);
return words[randIndex];
}
return (
<div>
<div> Word = {newWord} </div>
<div> Time = {time} </div>
</div>
)
}
ReactDOM.render( < App / > , document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
First of all I want to know if I am right about cause of the problem.
const updateScore = (isCorrect) => {
// Update Game Variables
if (isCorrect === true) {
counter++;
score += 100;
}
};
// Reset Styling
const resetLoadedQuestionStyling = (isCorrect) => {
questionScreen.style.display = 'none';
answerArr.forEach(answer => {
answer.classList.remove('correct');
answer.classList.remove('wrong');
answer.classList.remove('disable');
});
updateScore(isCorrect);
};
const styleAnswer = (div, isCorrect) => {
if (isCorrect === true) {
div.classList.add('correct');
} else {
div.classList.add('wrong');
for (let i = 0; i < answerArr.length; i++) {
if (i === currentQuestion.correct) {
answerArr[i].classList.add('correct');
}
}
}
// Prevent Second Check
answerArr.forEach(answer => {
answer.classList.add('disable');
});
// Reset Styling
setTimeout(() => {
resetLoadedQuestionStyling(isCorrect);
}, 3000);
};
const checkAnswer = (div, index) => {
const userChoice = index;
// Default Answer State
let isCorrect = false;
if (userChoice === currentQuestion.correct) {
isCorrect = true;
}
styleAnswer(div, isCorrect);
};
answerArr.forEach((div, index) => {
div.addEventListener('click', () => {
checkAnswer(div, index);
});
});
My counter updates 1,time, that 2 times... and I think the cause of this issue is that my EventListener is in a forEach loop, is that right?
How to prevent it?
Thanks!
EDIT: Addded more of the code in order to get my idea better.
EDIT: answerArr is array of 4 divs in my HTML
There may be a setTimeout-related issue. Every time an answer is clicked, the counter is set to be incremented after 3 seconds.
Here's the sequence when an answer is clicked:
'click'
checkAnswer ->
styleAnswer ->
setTimeout =>
resetLoadedQuestionStyling ->
updateScore ->
counter++
Below is the code with all of the unrelated lines removed. It does increment the counter after every click, but only after 3 seconds.
const answerArr = [...document.querySelectorAll('button')];
let counter = 0;
const span = document.getElementById('counter');
const updateScore = (isCorrect) => {
if (isCorrect === true) {
counter++
}
span.innerText = counter;
}
const resetLoadedQuestionStyling = (isCorrect) => {
updateScore(isCorrect)
}
const styleAnswer = (div, isCorrect) => {
// Reset Styling after 3 seconds
setTimeout(() => {
resetLoadedQuestionStyling(isCorrect);
}, 3000);
}
const checkAnswer = (div, index) => {
styleAnswer(div, true);
}
answerArr.forEach((div, index) => {
div.addEventListener('click', () => {
checkAnswer(div, index);
});
});
<button>Answer 1</button><br>
<button>Answer 2</button><br>
<button>Answer 3</button><br>
<button>Answer 4</button><br>
<p>Counter: <span id="counter"></span></p>
I am trying to access to intervalId "useState" variable inside of a timeInterval function. But the scope is not working properly. intervalId is always null inside ot the timeInterval function, that means that it does not about delay of value assignation.
export function useLocalCommands(length, id) {
const [intervalId, setIntervalId] = useState(null)
const [currentIndex, setCurrentIndex] = useState(10)
const [timeInterval, setTimeInterval] = useState(Constants.READING.READING_TIME_INTERVAL)
let pauseReading = () => {
console.log("pause:" + intervalId)
clearInterval(intervalId)
}
let startReading = () => {
console.log("play")
pauseReading()
if (currentIndex < length) {
setIntervalId(setInterval(() => {
setCurrentIndex((index) => {
if (index < length) {
if (id && index % 40 === 0) {
Meteor.call('myBooks.updateIndex', id, index, (err, res) => {
// show a toast if err
})
}
return index + 1
} else {
console.log("pauseReading: " + intervalId)
pauseReading();
}
})
}, timeInterval))
}
}
}
Thanks you,
Best.
IntervalId is being used from closure which is why when the setInterval runs, the values being takes at the time of declaration. However setIntervalId triggeres a state update and even though the value of state is updated, your timerId within setInterval function continues to point to the old state that it used from closure.
Instead of using state, you can make use of useRef to store the timerId. Since refs are mutated, they aren't affected by closure
export function useLocalCommands(length, id) {
const intervalId = useRef(null)
const [currentIndex, setCurrentIndex] = useState(10)
const [timeInterval, setTimeInterval] = useState(Constants.READING.READING_TIME_INTERVAL)
let pauseReading = () => {
console.log("pause:" + intervalId.current)
clearInterval(intervalId.current)
}
let startReading = () => {
console.log("play")
pauseReading()
if (currentIndex < length) {
intervalId.current = setInterval(() => {
setCurrentIndex((index) => {
if (index < length) {
if (id && index % 40 === 0) {
Meteor.call('myBooks.updateIndex', id, index, (err, res) => {
// show a toast if err
})
}
return index + 1
} else {
console.log("pauseReading: " + intervalId.current)
pauseReading();
}
})
}, timeInterval);
}
}
}
I am playing around with React Hooks, calling a method (that mutates state) from two different useEffect. Following code is given:
function App() {
const [clicked, setClicked] = useState(false);
/**
* Listen for clicked changes. When clicked changes to true,
* allow setCounterAndSetUrlHash to do it's thing, before accpeting
* the next click. So the clicked flag is simply a valve, that opens
* after two seconds.
*/
useEffect(() => {
if (clicked) {
setCounterAndSetUrlHash(counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
const [counter, setCounter] = useState(0);
/**
* Listen for changes in the URL hash. When the user presses
* the back button in the browser toolbar, decrement the
* counter value.
*/
useEffect(() => {
window.onhashchange = () => {
const value = Number(window.location.hash.replace("#", ""));
// must be number
if (typeof value === "number" && value % 1 === 0) {
if (counter - 1 === value) {
setCounterAndSetUrlHash(counter - 1);
}
}
};
});
/**
* Set a new counter value and apply the same value
* to the URL hash. I want to reuse this function
* in both useEffect above.
*/
const setCounterAndSetUrlHash = value => {
setCounter(value);
if (value === 0) {
window.location.hash = "";
} else {
window.location.hash = String(value);
}
};
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
The code in action: https://codesandbox.io/s/dreamy-shadow-7xesm
The code is actually working. However I am getting this warning..
React Hook useEffect has a missing dependency: 'counter'. Either
include it or remove the dependency array.
(react-hooks/exhaustive-deps)
.. and I am not sure how to conform with that while keeping the current functionality. When I add counter to the dependencies, I end up with an infinite loop.
Your first effect uses counter state variable but its dependency list does not include it. Including it in dependency list will create infinite loop.
You can remove the dependency on counter by using function type argument in setCounter.
function App() {
const [clicked, setClicked] = useState(false);
/**
* Listen for clicked changes. When clicked changes to true,
* allow setCounterAndSetUrlHash to do it's thing, before accpeting
* the next click. So the clicked flag is simply a valve, that opens
* after two seconds.
*/
useEffect(() => {
if (clicked) {
incrCounter(1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
const [counter, setCounter] = useState(0);
/**
* Listen for changes in the URL hash. When the user presses
* the back button in the browser toolbar, decrement the
* counter value.
*/
useEffect(() => {
window.onhashchange = () => {
const value = Number(window.location.hash.replace("#", ""));
// must be number
if (typeof value === "number" && value % 1 === 0) {
if (counter - 1 === value) {
incrCounter(- 1);
}
}
};
});
useEffect(() => {
if (counter === 0) {
window.location.hash = "";
} else {
window.location.hash = String(counter);
}
}, [counter])
/**
* Set a new counter value and apply the same value
* to the URL hash. I want to reuse this function
* in both useEffect above.
*/
const incrCounter = delta => {
setCounter(value => value + delta);
};
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
}
Try using the functional setState, setState((state, props) => stateChange)
useEffect(() => {
if (clicked) {
setCounterAndSetUrlHash(counter => counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
To solve the issue of the onhashchange callback using the first counter value I suggest to move the functionality to the callback of setCounter. This would also imply that you need a different function for the button and the hash change.
Also set the variables and useState definitions at the top, and after useEffect which can make use of them. If you want a useEffect to run only once, set an empty array of dependencies; leaving out dependecies will run on every render.
export const App = () => {
const [clicked, setClicked] = useState(false);
const [counter, setCounter] = useState(0);
/**
* Listen for clicked changes. When clicked changes to true,
* allow setCounterAndSetUrlHash to do it's thing, before accpeting
* the next click. So the clicked flag is simply a valve, that opens
* after two seconds.
*/
useEffect(() => {
if (clicked) {
setCounter(counter => {
const value = counter + 1;
if (value === 0) {
window.location.hash = "";
} else {
window.location.hash = String(value);
}
return value;
});
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked]);
/**
* Listen for changes in the URL hash. When the user presses
* the back button in the browser toolbar, decrement the
* counter value.
*/
useEffect(() => {
window.onhashchange = e => {
const value = Number(window.location.hash.replace("#", ""));
// must be number
if (typeof value === "number" && value % 1 === 0) {
setCounter(counter => {
if (counter - 1 !== value) {
return counter;
}
if (value === 0) {
window.location.hash = "";
} else {
window.location.hash = String(value);
}
return value;
});
}
};
}, []);
return (
<div className="App">
<p>Clicked: {String(clicked)}</p>
<p>Counter: {counter}</p>
<button type="button" onClick={() => setClicked(true)}>
Click me
</button>
</div>
);
};
Add counter to the first useEffect():
const [counter, setCounter] = useState(0);
useEffect(() => {
if (clicked) {
setCounterAndSetUrlHash(counter + 1);
setTimeout(() => {
setClicked(false);
}, 2000);
}
}, [clicked, counter]);
https://codesandbox.io/s/cold-sun-56u7j