why my timer component is not working as a timer its time is not reducing - reactjs

i have a timer which shows the timer based on the time left by comparing it from now to the time received from server and shows timer what i have done is below, received time is the string like "2020-09-02T05:09:56.119Z" now i want the time to be the difference between time received from server and the time now but my timer is showing two times only as shown in the below gif
Link to the problem timer
import React, {useState, useEffect} from 'react';
import {Box, Message, Video, Timer, BlueScreen, Emoji, Heading, SubHeading, WaitingImage} from './styled' ;
import Button from 'Components/Button';
import { getAPatient } from '../api';
import moment from 'moment';
export default function WaitingRoom(){
const { t } = useTranslation();
const [timeLeft, setTimeLeft] = useState();
const url = window.location.href.split('?id=');
const id = url[1];
const geTimerTime = async () => {
await getAPatient(id)
.then((info) => {
const datetime = info && info.data.datetime;
const currentTime = moment().toISOString()
const d1 = new Date(currentTime);
const d2 = new Date(datetime);
const difference = d1 - d2;
if (difference > 60e3){
const minutes = Math.floor(difference / 60e3);
const seconds = minutes * 60;
setTimeLeft(seconds)
}
else {
const seconds = Math.floor(difference / 1e3);
setTimeLeft(seconds)
}
console.log(currentTime, datetime,difference,"infopoooo")
})
.catch((err) => {
console.log(err)
});
}
useEffect(() => {
geTimerTime()
if (!timeLeft) return;
}, [timeLeft]);
return(
timeLeft === 0 ?
<BlueScreen>
<Emoji>
<Smiley />
</Emoji>
<Heading>
{t('turnMessageHeading')}
</Heading>
<SubHeading>
{t('turnMessageSubHeading')}
</SubHeading>
<SubHeading>
{t('turnMessageSubHeadingDoctor')}
</SubHeading>
<Button
themeWhite
>
{t('commingMessage')}
</Button>
</BlueScreen>
:
<Box>
<Button
themeBlue
width={'100%'}
textAlign={'left'}
>
{t('Virtual_waiting_room')}
</Button>
<Message>
{t('waiting_message')}
</Message>
<Timer>
<div>{Math.floor(timeLeft/60) + ':' + ('0' + Math.floor(timeLeft % 60)).slice(-2)}</div> minutes
</Timer>
<WaitingImage>
<Waiting />
</WaitingImage>
<Video>
<iframe src='https://www.youtube.com/embed/gaka1vqYFNs'
frameborder='0'
allow='autoplay; encrypted-media'
allowfullscreen
title='video'
width={"100%"}
/>
</Video>
</Box>
)
}

It is simply because you call setTimeLeft, which sets the state for timeLeft, which in react triggers a re-render, during which all the useEffect() hooks will run, except for those that are not listening to changes on a state variable.
Furthermore, you have timeLeft in the dependency array (the second parameter in the hook), which means this useEffect() will run on every re-render where timeLeft state was set, thus running on each iteration.
If you need your useEffect() to run like componentDidMount, you need to keep an empty dependency array, thus ensuring the hook only runs on initial render
useEffect(() => {
geTimerTime()
if (!timeLeft) return;
}, []);//empty dependency array to mimic componentDidMount

useEffect's set state Function should not match the second parameter. The useEffects is run whenever any data inside the second parameter's array is changed. Since you are changing the timeLeft inside the useEffects it will run infinitely. If you want to do geTimerTime anyway and have the timeLeft inside the dependency array(second parameter to useEffect) then you can do like below
useEffect(() => {
if (timeLeft) {
geTimerTime();
}
}, [timeLeft])

Related

How do I test for DOM changes created by a setInterval function when using Jest/React testing library?

I created a simple countdown component that I would like to test is working correctly with React testing library. Everything works as expected in a browser but when testing the rendered output never advances more than one second regardless of the values used.
Component (custom hook based on Dan Abramovs blog post):
import React, { useEffect, useRef, useState } from "react"
import TimerButton from "../TimerButton/TimerButton"
// custom hook
function useInterval(callback, delay, state) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (state.isOn) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [state])
}
const Timer = () => {
const initialState = {
minutes: 25,
seconds: 0,
isOn: false,
}
const [timerData, setTimerData] = useState(initialState)
useInterval(
() => {
if (timerData.seconds === 0) {
if (timerData.minutes === 0) {
setTimerData({ ...timerData, isOn: false })
} else {
setTimerData({
...timerData,
minutes: timerData.minutes - 1,
seconds: 59,
})
}
} else {
setTimerData({ ...timerData, seconds: timerData.seconds - 1 })
}
},
1000,
timerData,
)
const displayedTime = `${
timerData.minutes >= 10 ? timerData.minutes : `0${timerData.minutes}`
}:${timerData.seconds >= 10 ? timerData.seconds : `0${timerData.seconds}`}`
const startTimer = () => {
setTimerData({ ...timerData, isOn: true })
}
const stopTimer = () => {
setTimerData({ ...timerData, isOn: false })
}
const resetTimer = () => {
setTimerData(initialState)
}
return (
<div className="timer__container" data-testid="timerContainer">
<div className="timer__time-display" data-testid="timeDisplayContainer">
{displayedTime}
</div>
<div className="timer__button-wrap">
<TimerButton buttonAction={startTimer} buttonValue="Start" />
<TimerButton buttonAction={stopTimer} buttonValue="Stop" />
<TimerButton buttonAction={resetTimer} buttonValue="Reset" />
</div>
</div>
)
}
export default Timer
And the test (there is a beforeEach and afterEach calling jest.useFakeTimers() and jest.useRealTimers() respectively above):
it("Assert timer counts down by correct interval when starting timer", async () => {
render(<Timer />)
const initialTime = screen.getByText("25:00")
const startTimerButton = screen.getByText("Start")
expect(initialTime).toBeInTheDocument() // This works fine
fireEvent.click(startTimerButton)
act(() => {
jest.advanceTimersByTime(5000)
})
await waitFor(() => expect(screen.getByText("24:55")).toBeInTheDocument())
})
Output of the test:
Unable to find an element with the text: 24:55. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div
class="timer__container"
data-testid="timerContainer"
>
<div
class="timer__time-display"
data-testid="timeDisplayContainer"
>
24:59 <--- This value is always the same
</div>
<div
class="timer__button-wrap"
>
{ ... }
</div>
</div>
</body>
Id like to assert that the correct time is being displayed after moving the time forward by different increments, if I can do this then it will allow for a much more comprehensive set of tests.
I have tried using many of the timing methods in Jest including advanceTimersByTime(), runOnlyPendingTimers(), runAllTimers() to no avail, I can never get the time to advance by more than 1 second. I have also added console.log()s in at various points in the useInterval function and can verify that the function is being called a varying number of times depending on how much I try to advance the time by (it gets called numbers of seconds + 1 times as expected).
When logging out the state data I have seen that the timerData state does not update except for the last run through:
callback {"minutes":25,"seconds":0,"isOn":true} // Every time but the last
callback {"minutes":24,"seconds":59,"isOn":true} // The last time only
Im thinking it must be something to do with how Im referencing the state values during update, or how Jests fake timers interact with state updates though I really dont know at this point
So, after a lot of playing and trying many different methods I have a solution for this.
Further investigation into the calling of the useInterval() function called showed that:
the correct lines to update state were being reached, but the component was only re-rendering once after all these loops of the setInterval function despite the number of updates to state that were supposedly triggered (verified using console logs)
altering the time passed to setInterval (i) to be less than 500ms showed that the time was advancing by amounts roughly equivalent to 1 % i
the component only re-renders once for each tick of the setInterval function that occurs under the time passed to jest.advanceTimersByTime()
From this it appears that for each call of jest.advanceTimersByTime() only one iteration of the setInterval function can be moved through, and so the only way I found to move the interval forward by the correct amount of time is to call advanceTimersByTime() repeatedly via a helper function:
const advanceJestTimersByTime = (increment, iterations) => {
for (let i = 0; i < iterations; i++) {
act(() => {
jest.advanceTimersByTime(increment)
})
}
}
Test updated to:
it("Assert timer counts down by correct interval when starting timer", async () => {
render(<Timer />)
const initialTime = screen.getByText("25:00")
const startTimerButton = screen.getByText("Start")
expect(initialTime).toBeInTheDocument()
fireEvent.click(startTimerButton)
advanceJestTimersByTime(1000, 60)
await waitFor(() => expect(screen.getByText("24:00")).toBeInTheDocument())
})
This passes and allows for assertions to be made at any point for the countdown

Call function when time changes in react

Being new to react , this all is really confusing and new to me , so I apologise if I'm making some obvious oversight.
Im making a stopwatch and implementing the seconds for starters. However; Im confused as to how i'll implement the on display seconds number to update when each second passes.
This is what I'm doing right now
function App() {
const [time , updateTime] = React.useState(0);
var startsec = 0;
//UpdateTime should get triggered when next second passes
const UpdateTime = () => {
//Update time variable with the new seconds elapsed
}
//Should run every second or something
const CheckTimeUpdation = () => {
currentsec = Math.floor(Date.now() / 1000.0);
console.log(currentsec);
if(currentsec > startsec){
UpdateTime(currentsec-startsec);
}
}
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
}
//Clock component just gets a number and displays it on the screen
return (<div className = "App">
<Clock timerSeconds= {time}/>
<div>
<button onClick={GetStartTime}></button>
</div>
</div>);
}
export default App;
Date.now() function gets the miliseconds passed since 1970 (hence the division by 1000 to make them into seconds) and I find the difference between when the button was clicked and current one and passs that to the time component to display.
How do I make the CheckTimeUpdation function run every second or so?
What you want is the setInterval() method (see: https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
However your code so far has some issues:
On the button click, getStartTime runs and it updates the value of startsec. Firstly, this does not cause the component to re-render, and so the component will not update and you will see nothing changing on your screen. Also, if you did get your component to re-render, you will notice that startsec will be 0 again on the next re-render, so re-assigning startsec like how you did likely doesn't do what you want it to. If you want to persist values between rerenders, you can use useState (https://reactjs.org/docs/hooks-reference.html#usestate) or useRef (https://reactjs.org/docs/hooks-reference.html#useref).
Now i'm assuming you want to start the timer when the button is clicked. What you need is to start the interval (via setInterval) on the button click, and update time every 1000ms.
You'd also want to clear the interval once you don't need it anymore, using the clearInterval() method. You'll need to save the id returned from setInterval() initially in order to clear it later.
Below I have written a short example using this idea with setInterval on button click to help you:
import { useState, useRef } from "react";
export default function App() {
const [timerState, setTimerState] = useState("paused");
const [timeElapsed, setTimeElapsed] = useState(0);
const intervalId = useRef(0);
const isRunning = timerState === "running";
const isPaused = timerState === "paused";
const onStartTimer = () => {
intervalId.current = setInterval(() => {
setTimeElapsed((time) => time + 1);
}, 1000);
setTimerState("running");
};
const onStopTimer = () => {
clearInterval(intervalId.current);
intervalId.current = 0;
setTimerState("paused");
};
const onClearTimer = () => {
setTimeElapsed(0);
};
return (
<div className="App">
<h1>{timeElapsed}</h1>
{isPaused && <button onClick={onStartTimer}>Start timer</button>}
{isRunning && <button onClick={onStopTimer}>Stop timer</button>}
{!!timeElapsed && <button onClick={onClearTimer}>Clear timer</button>}
</div>
);
}
You can use setInterval in the GetStartTime Function.
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
setInterval(() => CheckTimeUpdation(), 1000);
}

Pomodoro Timer REACTJS not working - useState not updating

I'm quite new to ReactJS and have been working on a Pomodoro Timer. How it works is that whenever the "Work Timer" reaches zero, it switches to the "Break Timer".
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
However,
this line: setSecondsLeft(nextSeconds); // This does not update secondsLeft
under useEffect, does not update the secondsLeft which results in the timer for the break to display wrongly.
Could use some advice to make this work.
Thanks!
import {
Button,
Heading,
VStack,
Stack,
HStack,
Box,
CircularProgress,
Text,
CircularProgressLabel,
} from '#chakra-ui/react';
import { useState, useRef, useContext, useEffect } from 'react';
import { SettingsContext } from '../helpers/SettingsContext';
import PomodoroSettings from './PomodoroSettings';
// Pomodoro
const Pomodoro = () => {
//Timer
const { newTimer, setNewTimer } = useContext(SettingsContext);
const [isPaused, setIsPaused] = useState(true);
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
const secondsLeftRef = useRef(secondsLeft);
const isPausedRef = useRef(isPaused);
const modeRef = useRef(newTimer.active);
function tick() {
secondsLeftRef.current--;
setSecondsLeft(secondsLeftRef.current);
}
useEffect(() => {
function switchMode() {
const nextMode = modeRef.current === 'work' ? 'break' : 'work';
const nextSeconds =
(nextMode === 'work' ? newTimer.work : newTimer.short) * 60;
setNewTimer({
work: newTimer.work,
short: newTimer.short,
long: newTimer.long,
active: nextMode,
});
modeRef.current = nextMode;
console.log(nextMode);
console.log('Next: ' + nextSeconds);
setSecondsLeft(nextSeconds); // This does not update secondsLeft
console.log('SecondsLeft: ' + secondsLeft);
secondsLeftRef.current = nextSeconds;
}
secondsLeftRef.current = newTimer.work * 60;
setSecondsLeft(secondsLeftRef.current);
const interval = setInterval(() => {
if (isPausedRef.current) {
return;
}
if (secondsLeftRef.current === 0) {
return switchMode();
}
tick();
}, 1000);
return () => clearInterval(interval);
}, [newTimer, setNewTimer]);
const totalSeconds =
newTimer.active === 'work' ? newTimer.work * 60 : newTimer.short * 60;
const percentage = Math.round((secondsLeft / totalSeconds) * 100);
const minutes = Math.floor(secondsLeft / 60);
let seconds = secondsLeft % 60;
if (seconds < 10) seconds = '0' + seconds;
// Start / Pause
const handleButton = (e) => {
e.preventDefault();
if (isPaused) {
setIsPaused(!isPaused);
isPausedRef.current = false;
} else {
setIsPaused(!isPaused);
isPausedRef.current = true;
}
};
// console.log(percentage)
// console.log(newTimer.active)
// console.log(secondsLeft)
// console.log(totalSeconds)
return (
<div>
<Heading as='h4' size='md'>
{' '}
Pomodoro{' '}
</Heading>
<VStack>
<HStack>
<Button variant='ghost'>Pomodoro</Button>
<Button variant='ghost'>Short Break</Button>
<Button variant='ghost'>Long Break</Button>
</HStack>
<CircularProgress
value={percentage}
color={newTimer.active === 'work' ? 'red.400' : 'green.400'}
size='200px'
thickness='10px'
>
<CircularProgressLabel>
<Text fontSize='3xl'>{minutes + ':' + seconds}</Text>
</CircularProgressLabel>
</CircularProgress>
<HStack>
<Button variant='outline' onClick={handleButton}>
{isPaused ? <div> START </div> : <div> STOP </div>}
</Button>
<PomodoroSettings />
</HStack>
</VStack>
</div>
);
};
export default Pomodoro;
useState uses array const [value, setValue] like this. It might be just an error in this declaration of newTimer.
There is too much going on in one eseEffect(). The initial value of newTimer.work is not modified so each re-render will set secondsLeftRef to the same initial value.
Since useEffect()updates a context object, the component will re-render on every iteration, setting secondsLeftRef to its initial value.
Not sure where the problem is but few things I noticed that you can try to fix or improve in your code -
Do not use or assign context or props value in useState as you are doing it for secondsLeft, reason is that useState is supposed to run once when component gets rendered first time and you might not get value for newTimer.work on very first render.
For this you can use useEffect and newTimer in dependency array.
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
You are trying to log secondsLeft immediatly after calling setSecondsLeft this will not give you correct value because setting state is asynchronous in react so it will not be available on next line after calling set state.
setSecondsLeft(nextSeconds);
console.log('SecondsLeft: ' + secondsLeft);
One last thing I noticed looking at your code is that it's possible that you might be registering multiple setInterval because below line is responsible for clearing interval only when component gets unmount but your useEffect will get called multiple times based on its dependencies.
return () => clearInterval(interval);
Try to debug your code for above 3rd point and if that's the problem you
can try clearing your interval in start of useEffect or add
some conditions to make sure it will get register once to get
expected results.
Note - I have not tried executing your code, but let me know if this helps or if I can improve my answer in any way.

setInterval in react does not update the setState

I want to make a countdown timer (25 minutes). This is the code in App.js file
import './App.css';
import React, { useState } from 'react';
function App() {
const [remainingTime, setRemainingTime] = useState(1500);
const [seconds, setSeconds] = useState(0);
const [minute, setMinute] = useState(remainingTime/60);
function timer() {
setRemainingTime(remainingTime-1);
let newMinute = remainingTime/60;
let minuteArray = [...newMinute.toString()];
setMinute(parseInt(minuteArray.slice(0,2).join("")));
setSeconds(remainingTime%60);
console.log(minute);
console.log(seconds);
}
return (
<div className="App">
<div className="pomodoro">
<div className="timer">{minute}:{seconds}</div>
<div className="button-container">
<button className="start" onClick={() => setInterval(timer, 1000)}>start</button>
</div>
</div>
</div>
);
}
export default App;
The interval does not update the state value. The minute value always 25 and the seconds value always 0. When I don't use setInterval and just use the timer function like this
<button className="start" onClick={timer}>start</button>
every time I click the start button, the value changes. Any idea? I know I should use clearInterval too but I don't know where to put it. Should I create a new function which contain setInterval and clearInterval?
Issue
The main issue is stale enclosures of state in the timer callback.
Within the interval callback, the standard form of setting a new value doesn't work because the callback keeps using the same (non-updated) state value each time it runs:
—this doesn't work: setValue(value + 1)
Solution
Use the alternate form of setting new value:
—this will work: setValue((value) => value + 1)
This form allows the callback to obtain a fresh value with the most recent state every time it runs.
Details
The minute and seconds is considered derived state (from remainingTime) so it shouldn't also be stored in state, it is easily computed from state.
Use a functional state update to update the remainingTime state from the previous state, not the state of the render cycle the callback was enqueued in.
Use a React ref to hold an interval timer ref, so the interval can be cleared.
Use an useEffect hook to return a clean up function to clear any running intervals when the component unmounts.
Code:
function App() {
const timerRef = React.useRef();
React.useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
const [remainingTime, setRemainingTime] = React.useState(1500);
function timer() {
setRemainingTime((remainingTime) => remainingTime - 1);
}
const startTimer = () => {
clearInterval(timerRef.current); // clear any running interval
setRemainingTime(1500); // reset state back to 25 minutes
timerRef.current = setInterval(timer, 1000); // start/restart interval
};
const minute = String(Math.floor(remainingTime / 60)).padStart(2, 0);
const seconds = String(remainingTime % 60).padStart(2, 0);
return (
<div className="App">
<div className="pomodoro">
<div className="timer">
{minute}:{seconds}
</div>
<div className="button-container">
<button className="start" onClick={startTimer}>
start
</button>
</div>
</div>
</div>
);
}
Demo
Since this will likely be a follow-up question, use another useEffect to "listen" to the remainingTime state and when the time hits 0, clear the interval, reset remainingTime back 1500 (for display), and show any alarm or alert or whatever for the pomodoro break/schedule.
On every rerender, timer is set as 1500. Use closure here, activated on click.
function timer() {
let t = remainingTime;
setInterval(() => {
t -= 1;
setRemainingTime(t);
let newMinute = t / 60;
let minuteArray = [...newMinute.toString()];
setMinute(parseInt(minuteArray.slice(0, 2).join("")));
setSeconds(t % 60);
console.log(minute);
console.log(seconds);
}, 1000); }

setInterval and React hooks produces unexpected results

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

Resources