Why does my setInterval run only once in react hooks? - reactjs

Why does this code only triggers the setInterval once and then stops...
const MainBar = ()=> {
const [clock, setClock] = useState("")
useEffect(() => {
const interval = setInterval(setClock(clockUpdate()), 1000);
console.log('Im in useEffect', clock)
});
...
Whereas passing it into another function makes it work each second like so ?
const MainBar = ()=> {
const [clock, setClock] = useState("")
useEffect(() => {
const interval = setInterval(()=>{setClock(clockUpdate())}, 1000);
console.log('Im in useEffect', clock)
});
...
Sorry I'm new to hooks and javascript.

setInterval requires a function to be passed for it to execute. It will execute the given function every second in this case. () => { setClock(clockUpdate()) } is actually an anonymous function; a function without a name. If you'd give it a proper name, it'd look like function updater() { setClock(clockUpdate()); }.
setInterval(setClock(clockUpdate()), 1000) doesn't work because setClock(clockUpdate()) is already executed, even before it is passed to setInterval. It cannot schedule it to run again, because it's not a function, it is a result already.

You can try this by adding the second parameter in useEffect which means if the clock changes, useEffect will run again
useEffect(() => {
const interval = setInterval(()=>{setClock(clockUpdate())}, 1000);
console.log('Im in useEffect', clock)
}, [clock]);

Related

React: Why do we need to utilize useEffect for timers?

According to the React docs as well as every example on stackoverflow for timers, people use something similar to Option 2 w/ useEffect (+useState) to create a timer that you can start/pause/reset.
However, I've also been able to create a timer in Option 1 by solely using useState.
Why does nobody rely on useState for timers? I understand that useEffect cleans up during unmounting/re-rendering, but does this really improve performance? Wouldn't the constant unmounting and remounting from useEffect and then calling setValue be slower than just executing a regular function that calls setValue? Both options can call clearInterval, so shouldn't either be sufficient for clean-up?
Also, which timer would be more "accurate", Option 1 or 2? I believe I understand how the Event Loop for async functions works, but in React it becomes a bit foggy to me. Would there ever be a case where multiple async functions are backlogged and somehow delay useEffect from triggering and making the timer in Option 2 tick at a slower rate than Option 1 (i.e. not ticking exactly every second and slowly lagging behind the other timer)?.
Thank you!
Option 1 - regular function + useState
const [time, setTime] = useState(1500);
const [startPauseBtnText, setStartPauseBtnText] = useState('START');
const timeID = useRef(null);
const startPauseTime = () => {
if (timeID.current) {
clearInterval(timeID.current);
timeID.current = null;
setStartPauseBtnText('START');
} else {
timeID.current = setInterval(() => {
setTime((prevTime) => {
return prevTime - 1;
});
}, 1000);
setStartPauseBtnText('PAUSE');
}
};
const resetTime = () => {
clearInterval(timeID.current);
timeID.current = null;
setTime(1500);
};
Option 2 - useEffect + useState
const [isActive, setIsActive] = useState(false);
const [time2, setTime2] = useState(1500);
useEffect(() => {
let timeID2;
if (isActive) {
timeID2 = setInterval(() => {
setTime2((prevTime) => {
return prevTime - 1;
});
}, 1000);
}
return () => clearInterval(timeID2);
}, [isActive, time2]);
const resetTime2 = () => {
setIsActive(false);
setTime2(1500);
};
useEffect allows you to clear the timer when your component unmounts (via the returned cleanup function) instead of leaving it running and triggering a future state update attempt for a component that isn’t there anymore.

Debounce or throttle with react hook

I need to make an api request when a search query param from an input fields changes, but only if the field is not empty.
I am testing with several answers found on this site, but can't get them working
Firstly this one with a custom hook
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
now in my component I do this
const debouncedTest = useDebounce(() => {console.log("something")}, 1000);
but this seems to gets called every rerender regardless of any parameter, and I need to be able to call it inside a useEffect, like this
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
which of course does not work
Another approach using lodash
const throttledTest = useRef(throttle(() => {
console.log("test");
}, 1000, {leading: false}))
But how would i trigger this from the useEffect above? I don't understand how to make this work
Thank you
Your hook's signature is not the same as when you call it.
Perhaps you should do something along these lines:
const [state, setState] = useState(''); // name it whatever makes sense
const debouncedState = useDebounce(state, 1000);
useEffect(() => {
if (debouncedState) functionCall(debouncedState);
}, [debouncedState])
I can quickly point out a thing or two here.
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
technically you can't do the above, useEffect can't be nested.
Normally debounce isn't having anything to do with a hook. Because it's a plain function. So you should first look for a solid debounce, create one or use lodash.debounce. And then structure your code to call debounce(fn). Fn is the original function that you want to defer with.
Also debounce is going to work with cases that changes often, that's why you want to apply debounce to reduce the frequency. Therefore it'll be relatively uncommon to see it inside a useEffect.
const debounced = debounce(fn, ...)
const App = () => {
const onClick = () => { debounced() }
return <button onClick={onClick} />
}
There's another common problem, people might take debounce function inside App. That's not correct either, since the App is triggered every time it renders.
I can provide a relatively more detailed solution later. It'll help if you can explain what you'd like to do as well.

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

React Native: Call function when a specific state changes

Is it possible to call a function I define whenever a specific state changes?
For example:
function Component(props) {
const [timerOn, setTimerOn] = useState(false);
function startTimer() {
setTimerOn(true);
setTimeout(() => setTimerOn(false), 1000)
}
startTimer();
}
I need to call startTimer whenever setTimerOn(false) is called. How do I do that without calling startTimer every time the screen is rendered?
useEffect is perfect here since you're already using React hooks. As stated in the official documentation -
The Effect Hook lets you perform side effects in function components
So in your case,
function Component(props) {
const [timerOn, setTimerOn] = useState(false);
function startTimer() {
setTimerOn(true);
setTimeout(1000, () => setTimerOn(false))
}
// This code is for it to run for the first time when your component mounts.
// Think of it as the previous componentDidMount function
useEffect(() => {
startTimer();
}, []);
// This code is for it to run whenever your variable, timerOn, changes
useEffect(() => {
if (!timerOn) {
startTimer();
}
}, [timerOn]); // The second parameters are the variables this useEffect is listening to for changes.
}
You can use the hook useEffect, this hook lets you execute code when any of the values inside the dependency array changes. You can use it like this
useEffect(()=> {
doSomethingWhenFooChanges();
},[foo]);
Edit to enrich answer:
You can do something like this:
function Component(props) {
const [timerOn, setTimerOn] = useState(false);
function startTimer() {
setTimerOn(true);
}
//Declaring timer variable
let timer;
useEffect(()=> {
if(!timerOn) {
timer = setTimeout(() => setTimerOn(false), 1000);
startTimer();
} else {
//To prevent memory leaks you must clear the timer
clearTimeout(timer);
}
}, [timerOn]);
}
In any case, I can't think of an scenario that you need to restart a timer when you can use setInterval. That function executes a function every 'n' seconds. And it's used like:
setInterval(()=> {
myFunctionToBeExecutedEvery1000ms();
}, 1000);
Regards
Since you are already using hooks. The useEffect hook will come to your rescue in this scenerio. More about it here

react hooks and setInterval

Is there any alternative to just keeping a "clock" in the background to implement auto-next (after a few seconds) in carousel using react hooks?
The custom react hook below implements a state for a carousel that supports manual (next, prev, reset) and automatic (start, stop) methods for changing the carousel's current (active) index.
const useCarousel = (items = []) => {
const [current, setCurrent] = useState(
items && items.length > 0 ? 0 : undefined
);
const [auto, setAuto] = useState(false);
const next = () => setCurrent((current + 1) % items.length);
const prev = () => setCurrent(current ? current - 1 : items.length - 1);
const reset = () => setCurrent(0);
const start = _ => setAuto(true);
const stop = _ => setAuto(false);
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, 3000);
return _ => clearInterval(interval);
});
return {
current,
next,
prev,
reset,
start,
stop
};
};
There are differences between setInterval and setTimeout that you may not want to lose by always restarting your timer when the component re-renders. This fiddle shows the difference in drift between the two when other code is also running. (On older browsers/machines—like from when I originally answered this question—you don't even need to simulate a large calculation to see a significant drift begin to occur after only a few seconds.)
Referring now to your answer, Marco, the use of setInterval is totally lost because effects without conditions dispose and re-run every time the component re-renders. So in your first example, the use of the current dependency causes that effect to dispose and re-run every time the current changes (every time the interval runs). The second one does the same thing, but actually every time any state changes (causing a re-render), which could lead to some unexpected behavior. The only reason that one works is because next() causes a state change.
Considering the fact that you are probably not concerned with exact timing, is is cleanest to use setTimeout in a simple fashion, using the current and auto vars as dependencies. So to re-state part of your answer, do this:
useEffect(
() => {
if (!auto) return;
const interval = setTimeout(_ => {
next();
}, autoInterval);
return _ => clearTimeout(interval);
},
[auto, current]
);
Generically, for those just reading this answer and want a way to do a simple timer, here is a version that doesn't take into account the OP's original code, nor their need for a way to start and stop the timer independently:
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
// You could also do `setCounter((count) => count + 1)` instead.
// If you did that, then you wouldn't need the dependency
// array argument to this `useEffect` call.
}, 1000);
return () => {
clearTimeout(id);
};
},
[counter],
);
However, you may be wondering how to use a more exact interval, given the fact that setTimeout can drift more than setInterval. Here is one method, again, generic without using the OP's code:
// Using refs:
const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
() => {
const id = setInterval(() => {
r.current.setCounter(r.current.counter + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
// Using the function version of `setCounter` is cleaner:
const [counter, setCounter] = useState(30);
useEffect(
() => {
const id = setInterval(() => {
setCounter((count) => count + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
Here is what is going on above:
(first example, using refs): To get setInterval's callback to always refer to the currently acceptable version of setCounter we need some mutable state. React gives us this with useRef. The useRef function will return an object that has a current property. We can then set that property (which will happen every time the component re-renders) to the current versions of counter and setCounter.
(second example, using functional setCounter): Same idea as the first, except that when we use the function version of setCounter, we will have access to the current version of the count as the first argument to the function. No need to use a ref to keep things up to date.
(both examples, continued): Then, to keep the interval from being disposed of on each render, we add an empty dependency array as the second argument to useEffect. The interval will still be cleared when the component is unmounted.
Note: I used to like using ["once"] as my dependency array to indicate that I am forcing this effect to be set up only once. It was nice for readability at the time, but I no longer use it for two reasons. First, hooks are more widely understood these days and we have seen the empty array all over the place. Second, it clashes with the very popular "rule of hooks" linter which is quite strict about what goes in the dependency array.
So applying what we know to the OP's original question, you could use setInterval for a less-likely-to-drift slideshow like this:
// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...
const r = useRef(null);
r.current = { next };
useEffect(
() => {
if (!auto) return;
const id = setInterval(() => {
r.current.next();
}, autoInterval);
return () => {
clearInterval(id);
};
},
[auto]
);
Because the current value is going to change on every "interval" as long as it should be running, then your code will start and stop a new timer on every render. You can see this in action here:
https://codesandbox.io/s/03xkkyj19w
You can change setInterval to be setTimeout and you will get the exact same behaviour. setTimeout is not a persistent clock, but it doesn't matter since they both get cleaned up anyways.
If you do not want to start any timer at all, then put the condition before setInterval not inside of it.
useEffect(
() => {
let id;
if (run) {
id = setInterval(() => {
setValue(value + 1)
}, 1000);
}
return () => {
if (id) {
alert(id) // notice this runs on every render and is different every time
clearInterval(id);
}
};
}
);
So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on auto and current to work
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
Conditionally executing update to state — it does not require useEffect dependencies
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, autoInterval);
return _ => clearInterval(interval);
});
Both solutions work if we replace setInterval by setTimeout
You could use useTimeout hook that returns true after specified number of milliseconds.
https://github.com/streamich/react-use/blob/master/docs/useTimeout.md

Resources