Facing problems in creating a Timer in React Js - reactjs

let [seconds,setSeconds] = useState(59) useEffect(()=>{ setInterval(()=>{ setSeconds(seconds-1) },1000) })
Passing {seconds} in html timer starts. But it works like 59-58 and then it is decreasing along with different numbers. I need solution for this.
I tried using loops and other methods but didn't work.
I was expecting 59-58-57-56 to 00

The issue is that every time seconds changes your components gets re-rendered creating multiple instances of setInterval.
You need to wrap your setInterval in a useEffect in order to be able to clear it properly.
React.useEffect(() => {
const timer =
seconds > 0 && setInterval(() => setSeconds(seconds - 1), 1000);
return () => clearInterval(timer);
}, [seconds]);

Related

Vitest issue with vi.spyOn() when used with clearInterval

I've created a Countdown timer component and am writing out tests. I want to test that the setInterval gets cleared when the distance is less than 0.
I've set up a test as follows using vi.spyOn(global, 'clearInterval'):
test('should clear the interval when the distance is less than 0', () => {
const date = setDate(SECOND);
vi.spyOn(global, 'clearInterval');
render(<CountdownClock endDate={date} etc.../>)
act(() => {
vi.runOnlyPendingTimers();
vi.runOnlyPendingTimers();
vi.clearAllMocks();
});
// expect assertions ...
});
The code itself handles the timer inside of a useEffect:
useEffect(() => {
// Start straight away (ie before first second elapses in setInterval)
if (distance() > 0) {
runTimer();
}
const interval = setInterval(() => {
if (distance() < 0) {
// Stop Timer
endTimer();
clearInterval(interval);
} else {
runTimer();
}
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
The above test sets the date to 1 second in the future and then I run setInterval twice more using vi.runOnlyPendingTimers(). This pushes the distance into negative which stops the timer and clears the interval.
I've logged this out and I can see that the spy works in this part of the code - I can see callCount of 1 after clearInterval has run.
The issue I get is that it then runs the useEffect return statement to clear up. At this point I get the error:
ReferenceError: clearInterval is not defined
❯ src/components/CountdownClock/CountdownClockContainer.tsx:94:12
92|
93| return () => {
94| clearInterval(interval);
| ^
95| };
I can't work out why clearInterval is undefined at this stage. My understanding is that spyOn doesn't create a mock and is rather 'spying' on the method so I can't figure out why clearInterval becomes undefined.
If anyone has any suggestions as to what might be the issue that would be great.

Issue clearing a recursive timeout with onClick in React

I'm rebuilding a special type of metronome I built in vanilla js, with React. Everything is working, except when a user clicks the 'STOP' button, the metronome doesn't stop. It seems I'm losing the timeout ID on re-renders, so clearTimeout is not working. This is a recursive timeout, so it calls itself after each timeout acting more like setInterval, except for it's adjusting the interval each time, thus I had to use setTimeout.
I've tried to save the timeoutID useing setState, but if I do that from within the useEffect hook, there's an infinite loop. How can I save the timerID and clear it onClick?
The code below is a simplifed version. The same thing is on codepen here. The codepen does not have any UI or audio assets, so it doesn't run anything. It's just a gist of the larger project to convey the issue.
You can also view the vanilla js version that works.
import { useState, useEffect } from 'React';
function startStopMetronome(props) {
const playDrum =
new Audio("./sounds/drum.wav").play();
};
let tempo = 100; // beats per minute
let msTempo = 60000 / tempo;
let drift;
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let timeout;
let expected;
const round = () => {
playDrum();
// Increment expected time by time interval for every round after running the callback function.
// The drift will be the current moment in time for this round minus the expected time.
let drift = Date.now() - expected;
expected += msTempo;
// Run timeout again and set the timeInterval of the next iteration to the original time interval minus the drift.
timeout = () => setTimeout(round, msTempo - drift);
timeout();
};
// Add method to start metronome
if (isRunning) {
// Set the expected time. The moment in time we start the timer plus whatever the time interval is.
expected = Date.now() + msTempo;
timeout = () => setTimeout(round, msTempo);
timeout();
};
// Add method to stop timer
if (!isRunning) {
clearTimeout(timeout);
};
});
const handleClick = (e) => {
setIsRunning(!isRunning);
};
return (
<div
onClick={handleClick}
className="start-stop"
children={isRunning ? 'STOP' : 'START'}>
</div>
)
}
Solved!
First, my timeouts didn't need the arrow functions. They should just be:
timeout = setTimeout(round, msTempo);
Second, a return in the useEffect block executes at the next re-render. The app will re-render (i thought is would be immediate). So, I added...
return () => clearTimeout(timeout);
to the bottom of the useEffect block.
Lastly, added the dependencies for my useEffect block to ensure it didn't fire on the wrong render.
[isRunning, subdivisions, msTempo, beatCount]);

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 Hooks: Idiomatic way to ensure that useEffect runs only when the contents of an array argument to the custom hook change

I am creating a custom hook in React which sets up an event listener for a given set of events. A default set of events exists, and the consumer of this custom hook is not expected to customize these in the majority of use-cases. Generally, I want the event listeners to be added upon the mounting of a component and removed upon its un-mounting. However, adhering to the principles of hooks (and the eslint(react-hooks/exhaustive-deps) lint rule), I wish to gracefully handle changes to the list of events to watch. What is the most idiomatic way to achieve this with React hooks?
Assuming I would simply like to remove all event listeners and re-add them when the list of events changes, I could attempt the following:
const useEventWatcher = (
interval = 5000,
events = ['mousemove', 'keydown', 'wheel']
) => {
const timerIdRef = useRef();
useEffect(() => {
const resetInterval = () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(`${interval} milliseconds passed with no ${events.join(', ')} events!`);
}, interval)
};
events.forEach(event => window.addEventListener(event, resetInterval));
// Don't want to miss the first time the interval passes without
// the given events triggering (cannot run the effect after every render due to this!)
resetInterval();
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval]);
}
Unfortunately, this will not function as intended. Note that I would like to provide a default value for the events parameter. Doing that with the current approach means that events points to a different reference every time the custom hook runs, which means the effect runs every time as well (due to shallow dependency comparison). Ideally, I would like a way of having the effect depend on the contents of the array, rather than the reference. What is the best way of achieving this?
You can separate two side effects in two different useEffects.
You can run the initial resetInterval in the first useEffect on load.
You need to run it once, and you might use a dependency of [].
But then, you need to extract resetInterval outside the useEffect.
Another problem is that, now resetInterval is re-created during every render.
So you can wrap it in useCallback.
The first useEffect depends on resetInterval (which causes the useEffect run once on load, thus will call resetInterval)
Now you can subscribe all events in the second useEffect with dependencies on [events, interval, resetInterval] as suggested by "eslint(react-hooks/exhaustive-deps) lint rule".
The result would look like following &
You can check the working demo on CodeSandbox.
const useEventWatcher = (
interval = 2000,
events = ["mousemove", "keydown", "wheel"]
) => {
const timerIdRef = useRef();
const resetInterval = useCallback(() => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
console.log(
`${interval} seconds passed with no ${events.join(", ")} events!`
);
}, interval);
}, [events, interval]);
useEffect(() => {
resetInterval();
}, [resetInterval]);
useEffect(() => {
events.forEach(event => window.addEventListener(event, resetInterval));
return () => {
events.forEach(event => window.removeEventListener(event, resetInterval));
};
}, [events, interval, resetInterval]);
};
Check out the working page (I set the interval to 2 seconds for demo purpose)
When you move the mouse around, scroll wheels or press keys, the console log won't appear. Once those events are not fired, then you will see the console log messages.
So we need to include events into deps but for sure we don't want endless render loop.
Option #1: use JSON.stringify or similar to pass string as dependency not an array
function useEventWatcher(events = ['click'])
useEffect(() => {
}, [JSON.stringifiy(events.sort())])
However ESLint will still complain so either suppress it or use de-stringify:
const eventsStringified = JSON.stringify(events.sort());
useEffect(() => {
const eventsUnstringified = JSON.parse(eventsStringified);
}, [eventStringified]);
Option #2: move setting default value into useMemo. So default values will be referentially the same while events parameter is not passed(so it is undefined)
function useEventWatcher(events) {
const eventsOrDefault = useMemo(() => eventsPassed || ['click'], [events]);
useEffect(() => {
}, [eventsOrDefault]);
}

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