I made a timer hook for react native which counts from from props to to props. It returns the current value of timer in seconds, start function, stop function, restart function and pause function in an object. Here is the timer code:
import { useState, useEffect, useRef } from 'react'
export default function useTimer({ from, to, intervalS, finished }) {
const [timer, setTimer] = useState(from)
const interval = useRef()
const start = () => {
if (!interval.current) {
interval.current = setInterval(() => {
setTimer(timer => timer - intervalS)
}, intervalS * 1000)
}
}
const pause = () => {
clearInterval(interval.current)
interval.current = null
}
const stop = () => {
clearInterval(interval.current)
interval.current = null
setTimer(from)
}
const restart = () => {
clearInterval(interval.current)
interval.current = null
setTimer(from)
start()
}
useEffect(() => {
return () => {
clearInterval(interval.current)
}
}, [])
useEffect(() => {
if (timer === to) {
finished()
clearInterval(interval.current)
}
}, [timer])
return {
value: timer,
start,
pause,
restart,
stop
}
}
Everything works perfectly but when i try to render (or even console.log) the timer value, it counts faster than what should be. For example the timer will pass 20 seconds for every 15 seconds in real time. I am using Expo Go app for development and my device is an android phone. Am i doing something wrong? Here is how i use timer:
export default function Countdown() {
const [finished, setFinished] = useState(false)
const timer = useTimer({ from: 60, to: 0, intervalS: 1, finished: () => setFinished(true) })
return (
<View style={{ alignItems: 'center' }}>
<Typography variant="heading">{timer.value}</Typography>
<Button title="start" onPress={timer.start} />
<Button title="pause" onPress={timer.pause} />
<Button title="restart" onPress={timer.restart} />
<Button title="stop" onPress={timer.stop} />
</View>
)
}
For anyone who might be wondering what's wrong, the problem is with expo's development mode. I don't know why but it seems timing is faster in expo's development mode. Just toggle the development mode to production mode and time will become normal
I used this approach to create a timer before:
const [intervalID, setIntervalID] = useState<any>(null)
//Initially set to 60 secs
const [testTime, setTestTime] = React.useState(60)
useEffect(() => {
setIntervalID(setInterval(() => {
updateTimer();
}, 1000));
return () => clearInterval(intervalID);
}, [])
useEffect(() => {
if (testTime <= 0) {
//Timer hit zero
return () => clearInterval(intervalID)
}
}, [testTime])
const updateTimer = () => {
setTestTime(testTime => testTime - 1)
}
function displayTime(seconds) {
const format = val => `0${Math.floor(val)}`.slice(-2)
const minutes = (seconds % 3600) / 60
return [minutes, seconds % 60].map(format).join(':')
}
const getRemainingTime = () => {
let finalTime = displayTime(testTime)
return <Text style={{ alignSelf: 'center' }}>{finalTime} minutes left</Text>
}
I had the same issue with my Expo app.
Just disable "Debug Remote JS" for it to work!
Related
I'm implementing stopwatch in ReactJs this is how my code looks as of now:
const App: React.FC = () => {
const [seconds, setSeconds] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const secondsToTimerFormat = (seconds: number): string => {
console.log(seconds)
return (seconds-seconds%60)/60+":"+seconds%60
}
const manipulateTimer = (toPauseTimer: boolean) => {
setIsPaused(toPauseTimer);
}
useEffect(() => {
if(!isPaused){
setTimeout(() => {
setSeconds(seconds + 1)
}, 1000)
}
}, [seconds, isPaused])
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button onClick={() => {manipulateTimer(true)}}>Pause</button>
<button onClick={() => {manipulateTimer(false)}}>Resume</button>
<button onClick={() => {
setSeconds(0);
}}>Reset</button>
</div>
</div>
);
}
I'm expecting this to work normally. But the "Reset" button is not working as expected.
If I click on "Reset" after 13 seconds, this is the console.log() output.
If I add a new variable inside useEffect(), say something like let execute: boolean = true and then set it to false in useEffect() clean up, everything is working as expected.
So, I know the fix, but I want to know the reason behind the current behaviour. I understand that when I click on reset, there is already a useEffect() running with seconds value as 13. But since its setTimeout() ends in one second and at the same time, I'm doing setSeconds(0), why would the previous useEffect() run multiple times before coming to halt?
Issues like this usually arise because the timers being used are not being cleared between renders. Also, when the next state depends on the current state, it is better to use the second form of the state setter function which takes the current state as the parameter and returns the next state. Modify the useEffect as given below to get this to work:
useEffect(() => {
let timer;
if (!isPaused) {
timer = setTimeout(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [seconds, isPaused]);
Try using setInterval and separate methods for handling the timer state:
import { useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [intervalId, setIntervalId] = useState(0);
const secondsToTimerFormat = (seconds) => {
console.log(seconds);
return (seconds - (seconds % 60)) / 60 + ":" + (seconds % 60);
};
const handleStart = () => {
const id = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
setIntervalId(id);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleReset = () => {
handlePause();
setSeconds(0);
};
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button
onClick={() => {
handlePause();
}}
>
Pause
</button>
<button
onClick={() => {
handleStart();
}}
>
Resume
</button>
<button
onClick={() => {
handleReset();
}}
>
Reset
</button>
</div>
</div>
);
}
Link to sandbox
I have a stopwatch function in React that I would like to stop after 15 minutes. I am not sure how to use clearInterval() in this case:
const [timer, setTimer] = useState(0);
const [isActive, setIsActive] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const countRef = useRef(null);
const lastUpdatedRef = useRef(null);
const [minutes,setMinutes] = useState(0)
const [seconds,setSeconds] = useState(0)
const timeCeiling = 900; //maximum minutes is 15
const timeFloor = 60; //maximum seconds is 60 so it resets after
useEffect(() => {
if (timer < timeCeiling) {
setMinutes(Math.floor(timer / 60));
setSeconds(timer % 60);
} else {
setMinutes(15);
setSeconds(0);
}
}, [timer]);
const handleStart = () => {
setIsActive(true);
setIsPaused(true);
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
The user clicks on the handleStart function which triggers a useEffect. It also has a lastUpdated dependency which triggers another function every 30 seconds.
The clock should end after 15:00 but it still continues after- where should I put clearInterval so that it stops the clock after 15 minutes? Or is there another way to do this?
I would place it in the useEffect that is running each time timer updates. Clear the interval in the else branch when the limit it hit.
useEffect(() => {
if (timer < timeCeiling) {
setMinutes(Math.floor(timer / 60));
setSeconds(timer % 60);
} else {
clearInterval(countRef.current);
setMinutes(15);
setSeconds(0);
}
}, [timer]);
You might also want to add an additional useEffect hook to clear any running timers should the component unmount before you manually clear them.
useEffect(() => {
return () => {
clearInterval(countRef.current);
clearInterval(lastUpdatedRef.current);
};
}, []);
You can add cleare interval in the else condition:
useEffect(() => {
if (timer < timeCeiling) {
setMinutes(Math.floor(timer / 60));
setSeconds(timer % 60);
} else {
setMinutes(15);
setSeconds(0);
countRef.current && clearInterval(countRef.current);
lastUpdatedRef.current && clearInterval(lastUpdatedRef.current);
}
}, [timer]);
And you should cleare interval when component un-mount:
useEffect(() => {
return () => {
countRef.current && clearInterval(countRef.current);
};
}, [countRef]);
useEffect(() => {
return () => {
lastUpdatedRef.current && clearInterval(lastUpdatedRef.current);
};
}, [lastUpdatedRef]);
I think you should use clearInterval in the else block in useEffect. Maybe this way:
else {
setMinutes(15);
setSeconds(0);
clearInterval(countRef.current) // I hope this works
}
I am trying to do a countdown timer but after it gets to 1 it resets to 5 when its supposed to go to '00:00', I don't know where I am going wrong please may someone help me
This is my code:
const CountDown = () => {
const RESET_INTERVAL_S = 5;
const formatTime = (time) =>
`${String(Math.floor(time / 60)).padStart(2, "0")}:${String(
time % 60
).padStart(2, "0")}`;
const Timer = ({ time }) => {
const timeRemain = RESET_INTERVAL_S - (time % RESET_INTERVAL_S);
return (
<>
<Text>{formatTime(timeRemain)}</Text>
</>
);
};
const IntervalTimerFunctional = () => {
const [time, setTime] = useState(0);
console.log("The time is", time);
useEffect(() => {
const timerId = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
return () => clearInterval(timerId);
}, []);
return <Timer time={time} />;
};
return <IntervalTimerFunctional />;
};
I am not sure this is a perfect solution but this could work:
You could stop your timer when it reaches its maximum value:
useEffect(() => {
const timerId = setInterval(() => {
setTime((t) => {
if(t + 1 === RESET_INTERVAL_S) {
clearInterval(timerId)
}
return t + 1;
});
}, 1000);
return () => clearInterval(timerId);
}, []);
And display "00:00" when you have reached the limit:
<Text>{time === RESET_INTERVAL_S ? "00:00" : formatTime(timeRemain)}</Text>
Here is a working example
useEffect(() => {
const timerId = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
return () => clearInterval(timerId);
}, []);
You need to set the value you want to repeat on. If not, this will keep resetting it.
You could also add a condition to check the value of the timer and stop it. like this:
{
time !== 0 ? setTime((t) => t+1): time = 0;
}
Here is a similar problem to yours.
I've ResetPassword component which renders Timer component, below are their code -
ResendPassword.js
class ResetPassword extends Component{
constructor(props){
super(props);
this.state = {
resendActive: false
};
}
endHandler(){
this.setState({
resendActive: true
})
}
render(){
return (
<Timer sec={5} counter={this.state.counter} end={this.endHandler.bind(this)}/>
)
}
}
Timer.js
const Timer = (props) => {
const [sec, setSec] = useState(props.sec);
useEffect(() => {
setSec(props.sec);
const intr = setInterval(() => {
setSec((s) => {
if(s > 0)
return --s;
props.end(); // Line: causing warning
clearInterval(intr);
return s;
});
}, 1000)
return () => {
clearInterval(intr);
}
}, [props.counter])
return (
<span>{sec > 60 ? `${Math.floor(sec/60)}:${sec - Math.floor(sec/60)}`: `${sec}`} sec</span>
)
}
In Above code I'm using timer in ResetPassword and I want a function call when timer ends so I'm passing endHandler as end in Timer component but calling that function giving - Warning: Cannot update during an existing state transition (such as within 'render'), can anyone let me know what I'm doing wrong here?
Thanks In Advance
Issue
setSec is a state update function and you use the functional state update variant. This update function callback is necessarily required to be a pure function, i.e. with zero side-effects. The invocation of props.end() is a side-effect.
Solution
Split out the side-effect invocation of props.end into its own effect hook so that it is independent of the state updater function.
const Timer = (props) => {
const [sec, setSec] = useState(props.sec);
useEffect(() => {
setSec(props.sec);
const intr = setInterval(() => {
setSec((s) => {
if (s > 0) return --s;
clearInterval(intr);
return s;
});
}, 1000);
return () => {
clearInterval(intr);
};
}, [props.counter]);
useEffect(() => {
console.log(sec);
if (sec <= 0) props.end(); // <-- move invoking `end` to own effect
}, [sec]);
return (
<span>
{sec > 60
? `${Math.floor(sec / 60)}:${sec - Math.floor(sec / 60)}`
: `${sec}`}{" "}
sec
</span>
);
};
Suggestion
Create a useInterval hook
const useInterval = (callback, delay) => {
const savedCallback = useRef(null);
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
const id = setInterval(savedCallback.current, delay);
return () => clearInterval(id);
}, [delay]);
};
Update Timer to use interval hook
const Timer = ({ end, sec: secProp}) => {
const [sec, setSec] = useState(secProp);
// Only decrement sec if sec !== 0
useInterval(() => setSec((s) => s - (s ? 1 : 0)), 1000);
useEffect(() => {
!sec && end(); // sec === 0, end!
}, [sec, end]);
return (
<span>
{sec > 60
? `${Math.floor(sec / 60)}:${sec - Math.floor(sec / 60)}`
: `${sec}`}{" "}
sec
</span>
);
};
So the timer works. If I hard code this.state with a specific countdown number, the timer begins counting down once the page loads. I want the clock to start counting down on a button click and have a function which changes the null of the state to a randomly generated number. I am a bit new to React. I am know that useState() only sets the initial value but if I am using a click event, how do I reset useState()? I have been trying to use setCountdown(ranNum) but it crashes my app. I am sure the answer is obvious but I am just not finding it.
If I didnt provide enough code, please let me know. I didn't want to post the whole shebang.
here is my code:
import React, { useState, useEffect } from 'react';
export const Timer = ({ranNum, timerComplete}) => {
const [ countDown, setCountdown ] = useState(ranNum)
useEffect(() => {
setTimeout(() => {
countDown - 1 < 0 ? timerComplete() : setCountdown(countDown - 1)
}, 1000)
}, [countDown, timerComplete])
return ( <p >Countdown: <span>{ countDown }</span> </p> )
}
handleClick(){
let newRanNum = Math.floor(Math.random() * 20);
this.generateStateInputs(newRanNum)
let current = this.state.currentImg;
let next = ++current % images.length;
this.setState({
currentImg: next,
ranNum: newRanNum
})
}
<Timer ranNum={this.state.ranNum} timerComplete={() => this.handleComplete()} />
<Button onClick={this.handleClick} name='Generate Inputs' />
<DisplayCount name='Word Count: ' count={this.state.ranNum} />
You should store countDown in the parent component and pass it to the child component. In the parent component, you should use a variable to trigger when to start Timer.
You can try this:
import React from "react";
export default function Timer() {
const [initialTime, setInitialTime] = React.useState(0);
const [startTimer, setStartTimer] = React.useState(false);
const handleOnClick = () => {
setInitialTime(5);
setStartTimer(true);
};
React.useEffect(() => {
if (initialTime > 0) {
setTimeout(() => {
console.log("startTime, ", initialTime);
setInitialTime(initialTime - 1);
}, 1000);
}
if (initialTime === 0 && startTimer) {
console.log("done");
setStartTimer(false);
}
}, [initialTime, startTimer]);
return (
<div>
<buttononClick={handleOnClick}>
Start
</button>
<Timer initialTime={initialTime} />
</div>
);
}
const Timer = ({ initialTime }) => {
return <div>CountDown: {initialTime}</div>;
};
useState sets the initial value just like you said, but in your case I don't think you want to store the countDown in the Timer. The reason for it is that ranNum is undefined when you start the application, and gets passed down to the Timer as undefined. When Timer mounts, useEffect will be triggered with the value undefined which is something you don't want since it will trigger the setTimeout. I believe that you can store countDown in the parent of the Timer, start the timeout when the button is clicked from the parent and send the countDown value to the Timer as a prop which would make the component way easier to understand.
Here is a simple implementation using hooks and setInterval
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(getRandomNum())
const [status, setStatus] = useState(STATUS.STOPPED)
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleRandom = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(getRandomNum())
}
useInterval(
() => {
if (secondsRemaining > 0) {
setSecondsRemaining(secondsRemaining - 1)
} else {
setStatus(STATUS.STOPPED)
}
},
status === STATUS.STARTED ? 1000 : null,
// passing null stops the interval
)
return (
<div className="App">
<h1>React Countdown Demo</h1>
<button onClick={handleStart} type="button">
Start
</button>
<button onClick={handleStop} type="button">
Stop
</button>
<button onClick={handleRandom} type="button">
Random
</button>
<div style={{padding: 20}}>{secondsRemaining}</div>
<div>Status: {status}</div>
</div>
)
}
function getRandomNum() {
return Math.floor(Math.random() * 20)
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
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])
}
Here is a link to a codesandbox demo: https://codesandbox.io/s/react-countdown-demo-random-c9dm8?file=/src/App.js