I'm learning about React hooks. One task to practice using the useRef and useEffect hooks was to build a "click counting" game. The game has a timer (which is powered by useEffect and setIinterval) that counts down from 10, and a state variable counts how many times you are able to click in that set amount of time.
I wanted to above and beyond and keep exploring so I wanted to add a button that would "reset" the game. I found that I had to add a state value to track whether the game is "active" or not (the game is not active when the countdown timer reaches 0). In order for the reset functionality to work I had to list this state value (called gameIsActive) in the useEffect dependency array. When the countdown timer reaches zero, the gameIsActive variable is switched from its default value of true to false, and clicking the reset button toggles it back to true, as well as resetting the other relevant state values (click count goes back to zero, timer goes back to 10, in this case).
What I'm struggling to understand is why this works. From the React docs on useEffect it would seem that adding gameIsActive to the dependency array should keep the effect from running, because during the game the value of gameIsActive does not change... The relevant wording in the docs:
In the example above, we pass [count] as the second argument. What does this mean? If the count is 5, and then our component re-renders with count still equal to 5, React will compare [5] from the previous render and [5] from the next render. Because all items in the array are the same (5 === 5), React would skip the effect. That’s our optimization.
When we render with count updated to 6, React will compare the items in the [5] array from the previous render to items in the [6] array from the next render. This time, React will re-apply the effect because 5 !== 6. If there are multiple items in the array, React will re-run the effect even if just one of them is different.
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function CounterGame() {
const [clickCount, setClickCount] = useState(0);
const [timeRemaining, setTimeRemaining] = useState(10);
const [gameIsActive, setGameIsActive] = useState(true);
const id = useRef(null);
const handleClick = () => {
setClickCount((clickCount) => clickCount + 1);
};
const handleReset = () => {
setTimeRemaining(10);
setClickCount(0);
setGameIsActive(true);
};
const clearInterval = () => {
window.clearInterval(id.current);
};
useEffect(() => {
id.current = window.setInterval(() => {
setTimeRemaining((timeRemaining) => timeRemaining - 1);
}, 1000);
return clearInterval;
// If gameIsActive is ommitted from the dependency array
// countdown timer will not restart when game is "reset"
}, [gameIsActive]);
useEffect(() => {
if (timeRemaining === 0) {
clearInterval();
}
setGameIsActive(false);
}, [timeRemaining]);
return (
<div className="App">
<h3>
Time remaining (secs):
{timeRemaining}
</h3>
<h3>
Click Count:
{clickCount}
</h3>
<button onClick={handleClick} disabled={!timeRemaining}>
Click Me
</button>
<button onClick={handleReset}>Reset Game</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<CounterGame />, rootElement);
If I exclude the gameIsActive from the dependency array on the first useEffect hook, the reset will not work if the counter hits zero. I'm operating on the guess that this is because when the timer hits zero, I clear the interval, and never re-instantiate it. Adding the gameIsActive state seemed necessary to trigger the effect to set another interval, so it makes sense when I go from false (when the timer hits zero) back to true (when I reset the "game"). But why does the effect run every time the timer ticks? I'm especially confused given I had to use the useRef hook to persist the interval ID from render to render, and that logic is occurring in the same hook.
Here is a link to a working codesandbox of the issue - you'll see that removing the gameIsActive value in the first useEffect's dependency array will cause the game to no longer work after the timer hits zero (although things will reset and start counting down if you click reset BEFORE the timer hits zero).
gameIsActivechanges (expected behavior)