As others have pointed out, the problem is that useState
is only called once (as deps = []
) to set up the interval:
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => window.clearInterval(timer);
}, []);
Then, every time setInterval
ticks, it will actually call setTime(time + 1)
, but time
will always hold the value it had initially when the setInterval
callback (closure) was defined.
You can use the alternative form of useState
's setter and provide a callback rather than the actual value you want to set (just like with setState
):
setTime(prevTime => prevTime + 1);
But I would encourage you to create your own useInterval
hook so that you can DRY and simplify your code by using setInterval
declaratively, as Dan Abramov suggests here in Making setInterval Declarative with React Hooks:
function useInterval(callback, delay) {
const intervalRef = React.useRef();
const callbackRef = React.useRef(callback);
// Remember the latest callback:
//
// Without this, if you change the callback, when setInterval ticks again, it
// will still call your old callback.
//
// If you add `callback` to useEffect's deps, it will work fine but the
// interval will be reset.
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval:
React.useEffect(() => {
if (typeof delay === 'number') {
intervalRef.current = window.setInterval(() => callbackRef.current(), delay);
// Clear interval if the components is unmounted or the delay changes:
return () => window.clearInterval(intervalRef.current);
}
}, [delay]);
// Returns a ref to the interval ID in case you want to clear it manually:
return intervalRef;
}
const Clock = () => {
const [time, setTime] = React.useState(0);
const [isPaused, setPaused] = React.useState(false);
const intervalRef = useInterval(() => {
if (time < 10) {
setTime(time + 1);
} else {
window.clearInterval(intervalRef.current);
}
}, isPaused ? null : 1000);
return (<React.Fragment>
<button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
{ isPaused ? 'RESUME ⏳' : 'PAUSE 🚧' }
</button>
<p>{ time.toString().padStart(2, '0') }/10 sec.</p>
<p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
</React.Fragment>);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
body, p {
margin: 0;
}
p + p {
margin-top: 8px;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
button {
margin: 32px 0;
padding: 8px;
border: 2px solid black;
background: transparent;
cursor: pointer;
border-radius: 2px;
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
Apart from producing simpler and cleaner code, this allows you to pause (and clear) the interval automatically by simply passing delay = null
and also returns the interval ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).
Actually, this could also be improved so that it doesn't restart the delay
when unpaused, but I guess for most uses cases this is good enough.
If you are looking for a similar answer for setTimeout
rather than setInterval
, check this out: https://mcmap.net/q/110735/-react-hooks-right-way-to-clear-timeouts-and-intervals.
You can also find declarative version of setTimeout
and setInterval
, useTimeout
and useInterval
, a few additional hooks written in TypeScript in https://www.npmjs.com/package/@swyg/corre.