I'm trying out the new React Hooks and have a Clock component with a time value which is supposed to increase every second. However, the value does not increase beyond one.
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
The reason is because the callback passed into setInterval's closure only accesses the time variable in the first render, it doesn't have access to the new time value in the subsequent render because the useEffect() is not invoked the second time.
time always has the value of 0 within the setInterval callback.
Like the setState you are familiar with, state hooks have two forms: one where it takes in the updated state, and the callback form which the current state is passed in. You should use the second form and read the latest state value within the setState callback to ensure that you have the latest state value before incrementing it.
Bonus: Alternative Approaches
Dan Abramov goes in-depth into the topic about using setInterval with hooks in his blog post and provides alternative ways around this issue. Highly recommend reading it!
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(prevTime => prevTime + 1); // <-- Change this line!
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
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/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/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://stackoverflow.com/a/59274757/3723993.
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.
useEffect function is evaluated only once on component mount when empty input list is provided.
An alternative to setInterval is to set new interval with setTimeout each time the state is updated:
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = setTimeout(() => {
setTime(time + 1);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [time]);
The performance impact of setTimeout is insignificant and can be generally ignored. Unless the component is time-sensitive to the point where newly set timeouts cause undesirable effects, both setInterval and setTimeout approaches are acceptable.
useRef can solve this problem, here is a similar component which increase the counter in every 1000ms
import { useState, useEffect, useRef } from "react";
export default function App() {
const initalState = 0;
const [count, setCount] = useState(initalState);
const counterRef = useRef(initalState);
useEffect(() => {
counterRef.current = count;
})
useEffect(() => {
setInterval(() => {
setCount(counterRef.current + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
and i think this article will help you about using interval for react hooks
An alternative solution would be to use useReducer, as it will always be passed the current state.
function Clock() {
const [time, dispatch] = React.useReducer((state = 0, action) => {
if (action.type === 'add') return state + 1
return state
});
React.useEffect(() => {
const timer = window.setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => {
if (seconds === 5) {
setSeconds(0);
return clearInterval(interval);
}
return (seconds += 1);
});
}, 1000);
}, []);
Note: This will help to update and reset the counter with useState hook. seconds will stop after 5 seconds. Because first change setSecond value then stop timer with updated seconds within setInterval. as useEffect run once.
This solutions dont work for me because i need to get the variable and do some stuff not just update it.
I get a workaround to get the updated value of the hook with a promise
Eg:
async function getCurrentHookValue(setHookFunction) {
return new Promise((resolve) => {
setHookFunction(prev => {
resolve(prev)
return prev;
})
})
}
With this i can get the value inside the setInterval function like this
let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time => time + 1);// **set callback function here**
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
Somehow similar issue, but when working with a state value which is an Object and is not updating.
I had some issue with that so I hope this may help someone.
We need to pass the older object merged with the new one
const [data, setData] = useState({key1: "val", key2: "val"});
useEffect(() => {
setData(...data, {key2: "new val", newKey: "another new"}); // --> Pass old object
}, []);
Do as below it works fine.
const [count , setCount] = useState(0);
async function increment(count,value) {
await setCount(count => count + 1);
}
//call increment function
increment(count);
I copied the code from this blog. All credits to the owner. https://overreacted.io/making-setinterval-declarative-with-react-hooks/
The only thing is that I adapted this React code to React Native code so if you are a react native coder just copy this and adapt it to what you want. Is very easy to adapt it!
import React, {useState, useEffect, useRef} from "react";
import {Text} from 'react-native';
function Counter() {
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest function.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
const [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <Text>{count}</Text>;
}
export default Counter;
const [loop, setLoop] = useState(0);
useEffect(() => {
setInterval(() => setLoop(Math.random()), 5000);
}, []);
useEffect(() => {
// DO SOMETHING...
}, [loop])
For those looking for a minimalist solution for:
Stop interval after N seconds, and
Be able to reset it multiple times again on button click.
(I am not a React expert by any means my coworker asked to help out, I wrote this up and thought someone else might find it useful.)
const [disabled, setDisabled] = useState(true)
const [inter, setInter] = useState(null)
const [seconds, setSeconds] = useState(0)
const startCounting = () => {
setSeconds(0)
setDisabled(true)
setInter(window.setInterval(() => {
setSeconds(seconds => seconds + 1)
}, 1000))
}
useEffect(() => {
startCounting()
}, [])
useEffect(() => {
if (seconds >= 3) {
setDisabled(false)
clearInterval(inter)
}
}, [seconds])
return (<button style = {{fontSize:'64px'}}
onClick={startCounting}
disabled = {disabled}>{seconds}</button>)
}
Tell React re-render when time changed.opt out
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, [time]);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
I am trying to build a timer with three buttons, a start, stop, where it stops on the current integer, and a reset. I have added my code below which results in the following issues.
I thought my stop function would stop the timer from decrementing but it continues to do so. Also, when logging my timer state in the console, you can see it does not update in the console even though it is updating in the DOM. Why is this?
Thank you for any insight at all.
import React from 'react';
import './style.css';
export default function App() {
const [timer, setTimer] = React.useState(50);
const reset = () => {
setTimer(50);
};
const start = () => {
setTimer((prev) => prev - 1);
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
setInterval(() => {
console.log('updated');
console.log(timer);
start();
}, 1000);
};
const stop = () => {
clearInterval(start);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}`
You have a small problem with assigning the actual value for the interval.
Here is how it should be in usage
const interval = setInterval(() => {})
clearInterval(interval)
For your code change, you can create a ref to keep the interval variable and use it to clean up the interval later.
function App() {
const [timer, setTimer] = React.useState(5);
const intervalRef = React.useRef(); //create a ref for interval
const reset = () => {
setTimer(5);
};
const start = () => {
setTimer((prev) => {
if(prev === 0) {
stop();
return 0;
}
return prev - 1;
});
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
//assign interval ref here
intervalRef.current = setInterval(() => {
start();
}, 1000);
};
const stop = () => {
//clear the interval ref
clearInterval(intervalRef.current);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</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>
clearTimeout or clearInterval each take a token, which was returned by a previous call to setTimeout or setInterval. So you'll need to store that token:
const id = setInterval(() => console.log("triggered"), 1000);
// ...
clearInterval(id)
Also, you should be careful about what happens if App is re-rendered, so you should probably put the set/clear logic inside useEffect so you can cleanup your interval.
Also also, although you didn't ask, your console.log(timer) isn't going to work, and will always print 50. The timer variable inside that callback is captured once, and is never updated because that callback is just inside the setInterval now. You'll need to clear and reset your interval with an updated callback function every time App re-renders, or use a ref that you keep updated, which is a pain.
I would recommend borrowing this custom hook that considers all of these things for you: https://usehooks-ts.com/react-hook/use-interval
Then your App component could become extremely simple, but still be robust:
const { useEffect, useRef, useState, useLayoutEffect } = React;
// https://usehooks-ts.com/react-hook/use-interval
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useLayoutEffect(() => {
savedCallback.current = callback;
}, [callback])
useEffect(() => {
if (!delay && delay !== 0) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [timer, setTimer] = useState(50);
const [running, setRunning] = useState(false);
useInterval(() => setTimer(t => t - 1), running ? 1000 : null);
const start = () => setRunning(true);
const stop = () => setRunning(false);
const reset = () => { setTimer(50); };
return (
<div>
<h1>{timer}</h1><button onClick={start}>start</button>
<button onClick={stop}> stop </button>
<button onClick={reset}> reset </button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("react"));
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
I think you should not wrap your setTimeout into a callable. Then you lose the ability to start and stop it because the variable does not reference the interval but the callable that wraps the interval.
Take a look at this guide: https://www.w3schools.com/jsref/met_win_clearinterval.asp
I've seen quite a few similar questions here, but none of the suggested solutions seemed to be working for me. Here's my problem (please notice that I'm very new to React, just learning it):
I have the following code in my App.js:
function App() {
const [movieSearchString, setMovieSearchString] = useState('')
const providerValue = {
moviesList: moviesList,
movieSearchString: movieSearchString,
}
const searchMovieHandler = () => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}
const movieInputChangeHandler = string => {
console.log('onMovieInputChange', string);
setMovieSearchString(string)
searchMovieHandler()
}
return (
<MoviesContext.Provider value={providerValue}>
<div className="App d-flex flex-column px-4 py-2">
<SearchBar
onMovieInputChange={movieInputChangeHandler}
/>
...Rest of the content
</div>
</MoviesContext.Provider>
);
}
In this situation all the console.logs get called EXCEPT the one that should be debounced (I tried both lodash debounce and my own, none worked; currently kept the lodash version).
So I tried to comment out that debounce call and tried to use it like that:
useEffect(() => {
console.log('use effect 1')
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}, [movieSearchString])
I'm getting the use effect 1 log when the movieSearchString changes, but not the debounced one.
So I tried to do this:
const debounced = useRef(debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000))
useEffect(() => debounced.current(movieSearchString), [movieSearchString])
In this case I'm getting the console log deb: after a second, but no movieSearchString is printed.
I don't know what else I can do here... Eventually what I want is when a user enters something in the text field, I want to send an API call with the entered string. I don't want to do it on every key stroke, of course, thus need the debounce. Any help, please?
Try to debounce the state value, not to debounce effect itself. It's easier to understand.
For example, you can use custom hook useDebounce in your project:
useDebounce.js
// borrowed from https://usehooks.com/useDebounce/
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
App.js
const [movieSearchString, setMovieSearchString] = useState('')
const debouncedMovieSearchString = useDebounce(movieSearchString, 300);
const movieInputChangeHandler = string => {
setMovieSearchString(string)
}
useEffect(() => {
console.log('see useDebounceValue in action', debouncedMovieSearchString);
}, [debouncedMovieSearchString]);
useEffect(() => {
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = debouncedMovieSearchString
}
callApi(params);
}, [debouncedMovieSearchString]);
Refer to this article: https://usehooks.com/useDebounce/
You need to wrap the function you want to debounce wit the debounce() function like this:
const searchMovieHandler = debounce(() => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
}, 1000);
I'm loading multiple animals into my ThreeJS project. All these animals have PositionalAudio with a setInterval function. I use them inside a useEffect function. On the callback I want to clear the interval, but it keeps calling the function.
This is the function where I set my setInterval:
const loadAudio = () => {
const animalSound = new THREE.PositionalAudio(listener);
animalSound.setBuffer(animalBuffer);
playSounds = setInterval(() => {
animalSound.play();
} , 5000);
audios.push(animalSound);
}
In the return function I try to clear the interval:
return () => {
audios.forEach((audio) => {
audio.stop();
clearInterval(playSounds);
});
};
Sadly the audio keeps playing every 5 seconds
Here is a code snippet
https://codesandbox.io/s/bitter-tree-bb4ld?file=/src/App.js
According to your code snippet, say you have Button:
<button
onClick={buttonToggle}
>
{start ? 'start' : 'stop'}
</button>
Initially we have some setup for useState and handle click function
const [seconds, setSeconds] = useState(0);
const [btnStart, setBtnStart] = useState(true);
const buttonToggle = useCallback(
() => setBtnStart(run => !run)
, []);
In the useEffect you will do following changes
useEffect(() => {
if(!btnStart) {
// setSeconds(0); // if you want to reset it as well
return;
}
const interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, [btnStart]);
Assuming you have a hook that has multiple useState, for example:
function useMyHook() {
const [s1, setS1] = useState();
const [s2, setS2] = useState();
const setFn = useCallback(
() => {
console.log('set s1');
setS1(Date.now());
console.log('set s2');
setS2(Date.now());
},
[setS1, setS2]
);
return setFn;
}
Now, depending of WHEN you call the setFn react will trigger either one or two rerender.
One rerender:
function MyComponent() {
console.log('render')
const setTwoStates = useMyHook()
// sync call => triggers only one re render
useEffect(() => setTwoStates(), [])
// console.logs are: render, set s1, set s2, render
return (
<div>hello</div>
);
}
Two rerenders:
function MyComponent() {
console.log('render')
const setTwoStates = useMyHook()
// as soon as the setFn is called in a later tick, react triggers two re renders
useEffect(() => setTimeout(setTwoStates, 1), [])
// console.logs are: render, set s1, render, set s2, render
return (
<div>hello</div>
);
}
Does anyone have an explanation for this? This can lead to unexpected behaviour depending how you call a hook.
I also created a small example repository in case you want to play around with this
https://github.com/JohannesMerz/react-setstate-rerenders
React will batch state setters when they set state in useEffect callbacks or event handlers (while the function is running) but in your async example the state is set after the effect function has returned.
const syncFn = () =>
console.log('in sync function');
const asyncFn = () => setTimeout(
()=>console.log('in async function'),
10
);
syncFn();
console.log('sync function returned');
asyncFn();
console.log('async function returned');
You can see in that snippet that in async function logs after async function returned so any set states that would happen would happen after the effect callback or event handler returned and cannot be batched unless you explicitly tell React to batch it.
You can use unstable_batchedUpdates to tell react to batch the updates:
const App = () => {
const [item1, setItem1] = React.useState(0);
const [item2, setItem2] = React.useState(0);
const render = React.useRef(0);
//mutate render ref (shows how often component is rendered)
render.current++;
const asyncUpdates = React.useCallback(() => {
Promise.resolve().then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setItem1((item) => item + 1);
setItem2((item) => item + 1);
});
});
}, []);
const syncUpdates = React.useCallback(() => {
setItem1((item) => item + 1);
setItem2((item) => item + 1);
}, []);
const asyncNonBatchedUpdates = React.useCallback(() => {
Promise.resolve().then(() => {
setItem1((item) => item + 1);
setItem2((item) => item + 1);
});
}, []);
//using asyncUpdates in effect on mount
React.useEffect(asyncUpdates, []);
return (
<div>
<h1>Rendered: {render.current}</h1>
<div>
<div>item1:{item1}</div>
<div>item2:{item2}</div>
</div>
{/* run async updates as event handler */}
<button onClick={asyncUpdates}>async updates</button>
<button onClick={syncUpdates}>sync updates</button>
<button onClick={asyncNonBatchedUpdates}>
async non bathed updates
</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>