(React)Child component pause its useEffect when the parent re-renders - javascript

Edit: This problem has been solved by adding a second parameter in the useEffect Hooks.
I am new to react and am building a toy react game. I borrowed this timer I found here. If user performs an action and the parent component re-renders, the timer stop counting down for a short period of time. According to (https://reactjs.org/docs/hooks-effect.html), react will remember the useEffect function, and call it later after performing the DOM updates. If that's the reason, are there any solutions or alternatives to this? If not, can anyone point me in the right direction? Any help will be appreciated!
Here is my code snippet for reference:
class Chalkboard extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 1,
seconds: 0,
addSeconds: 0,
};
}
updateTimer() {
this.setState({ addSeconds: 0 });
}
handleTimer() {
window.removeEventListener('keydown', this.keyHandling);
this.setState({
gameOver: true,
});
}
render() {
return(
<Timer
initialMinutes={this.state.minutes}
initialSeconds={this.state.seconds}
onTimer={this.handleTimer}
addSeconds={this.state.addSeconds}
updateTimer={this.updateTimer}
/>
)
}
import React, { useState, useEffect } from 'react';
const Timer = (props) => {
const { initialMinutes = 0, initialSeconds = 0 } = props;
const [minutes, setMinutes] = useState(initialMinutes);
const [seconds, setSeconds] = useState(initialSeconds);
useEffect(() => {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
else if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval);
} else {
setSeconds(59);
setMinutes(minutes - 1);
}
}
}, 1000);
return () => {
clearInterval(myInterval);
};
});
useEffect(() =>{
if(props.addSeconds>0){
setSeconds(seconds + props.addSeconds);
props.updateTimer()
}
if(minutes === 0 && seconds === 0){
props.onTimer()
}
});
return (
<div className = "timer-container" >
{minutes === 0 && seconds === 0 ? null: (
<div>
{' '}
{minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</div>
)}
</div>
);
};
export default Timer;

Can you provide an example on https://codesandbox.io/?
Try to pass [props, minutes, seconds] as a second parameter in useEffect hook.
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

Related

Is there a way of calling an html element before it is defined? [duplicate]

I have seen lots of countdown timers in JavaScript and wanted to get one working in React.
I have borrowed this function I found online:
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
};
And then I have written this code myself
initiateTimer = () => {
let timeLeftVar = this.secondsToTime(60);
this.setState({ timeLeft: timeLeftVar })
};
startTimer = () => {
let interval = setInterval(this.timer, 1000);
this.setState({ interval: interval });
};
timer = () => {
if (this.state.timeLeft >0){
this.setState({ timeLeft: this.state.timeLeft -1 });
}
else {
clearInterval(this.state.interval);
//this.postToSlack();
}
};
Currently onclick it will set the time on screen to: Time Remaining: 1 m : 0 s
But it does not reduce it to Time Remaining: 0 m : 59 s and then Time Remaining: 0 m : 58 s etc etc
I think I need to call the function again with a different parameter. how can I go about doing this ?
Edit: I forgot to say, I would like the functionality so that I can use seconds to minutes & seconds
You have to setState every second with the seconds remaining (every time the interval is called). Here's an example:
class Example extends React.Component {
constructor() {
super();
this.state = { time: {}, seconds: 5 };
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
}
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
componentDidMount() {
let timeLeftVar = this.secondsToTime(this.state.seconds);
this.setState({ time: timeLeftVar });
}
startTimer() {
if (this.timer == 0 && this.state.seconds > 0) {
this.timer = setInterval(this.countDown, 1000);
}
}
countDown() {
// Remove one second, set state so a re-render happens.
let seconds = this.state.seconds - 1;
this.setState({
time: this.secondsToTime(seconds),
seconds: seconds,
});
// Check if we're at zero.
if (seconds == 0) {
clearInterval(this.timer);
}
}
render() {
return(
<div>
<button onClick={this.startTimer}>Start</button>
m: {this.state.time.m} s: {this.state.time.s}
</div>
);
}
}
ReactDOM.render(<Example/>, document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>
Here is a solution using hooks, Timer component, I'm replicating same logic above with hooks
import React from 'react'
import { useState, useEffect } from 'react';
const Timer = (props:any) => {
const {initialMinute = 0,initialSeconds = 0} = props;
const [ minutes, setMinutes ] = useState(initialMinute);
const [seconds, setSeconds ] = useState(initialSeconds);
useEffect(()=>{
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval)
} else {
setMinutes(minutes - 1);
setSeconds(59);
}
}
}, 1000)
return ()=> {
clearInterval(myInterval);
};
});
return (
<div>
{ minutes === 0 && seconds === 0
? null
: <h1> {minutes}:{seconds < 10 ? `0${seconds}` : seconds}</h1>
}
</div>
)
}
export default Timer;
Here is a simple implementation using hooks and useInterval implementation of #dan-abramov
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
const INITIAL_COUNT = 120
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(INITIAL_COUNT)
const [status, setStatus] = useState(STATUS.STOPPED)
const secondsToDisplay = secondsRemaining % 60
const minutesRemaining = (secondsRemaining - secondsToDisplay) / 60
const minutesToDisplay = minutesRemaining % 60
const hoursToDisplay = (minutesRemaining - minutesToDisplay) / 60
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleReset = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(INITIAL_COUNT)
}
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={handleReset} type="button">
Reset
</button>
<div style={{padding: 20}}>
{twoDigits(hoursToDisplay)}:{twoDigits(minutesToDisplay)}:
{twoDigits(secondsToDisplay)}
</div>
<div>Status: {status}</div>
</div>
)
}
// 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])
}
// https://stackoverflow.com/a/2998874/1673761
const twoDigits = (num) => String(num).padStart(2, '0')
Here is the codesandbox implementation: https://codesandbox.io/s/react-countdown-demo-gtr4u?file=/src/App.js
Basic idea showing counting down using Date.now() instead of subtracting one which will drift over time.
class Example extends React.Component {
constructor() {
super();
this.state = {
time: {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
},
duration: 2 * 60 * 1000,
timer: null
};
this.startTimer = this.start.bind(this);
}
msToTime(duration) {
let milliseconds = parseInt((duration % 1000));
let seconds = Math.floor((duration / 1000) % 60);
let minutes = Math.floor((duration / (1000 * 60)) % 60);
let hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
milliseconds = milliseconds.toString().padStart(3, '0');
return {
hours,
minutes,
seconds,
milliseconds
};
}
componentDidMount() {}
start() {
if (!this.state.timer) {
this.state.startTime = Date.now();
this.timer = window.setInterval(() => this.run(), 10);
}
}
run() {
const diff = Date.now() - this.state.startTime;
// If you want to count up
// this.setState(() => ({
// time: this.msToTime(diff)
// }));
// count down
let remaining = this.state.duration - diff;
if (remaining < 0) {
remaining = 0;
}
this.setState(() => ({
time: this.msToTime(remaining)
}));
if (remaining === 0) {
window.clearTimeout(this.timer);
this.timer = null;
}
}
render() {
return ( <
div >
<
button onClick = {
this.startTimer
} > Start < /button> {
this.state.time.hours
}: {
this.state.time.minutes
}: {
this.state.time.seconds
}. {
this.state.time.milliseconds
}:
<
/div>
);
}
}
ReactDOM.render( < Example / > , document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>
simple resolution:
import React, { useState, useEffect } from "react";
const Timer = ({ delayResend = "180" }) => {
const [delay, setDelay] = useState(+delayResend);
const minutes = Math.floor(delay / 60);
const seconds = Math.floor(delay % 60);
useEffect(() => {
const timer = setInterval(() => {
setDelay(delay - 1);
}, 1000);
if (delay === 0) {
clearInterval(timer);
}
return () => {
clearInterval(timer);
};
});
return (
<>
<span>
{minutes}:{seconds}
</span>
</>
);
};
export default Timer;
The problem is in your "this" value.
Timer function cannot access the "state" prop because run in a different context. I suggest you to do something like this:
...
startTimer = () => {
let interval = setInterval(this.timer.bind(this), 1000);
this.setState({ interval });
};
As you can see I've added a "bind" method to your timer function. This allows the timer, when called, to access the same "this" of your react component (This is the primary problem/improvement when working with javascript in general).
Another option is to use another arrow function:
startTimer = () => {
let interval = setInterval(() => this.timer(), 1000);
this.setState({ interval });
};
Countdown of user input
Interface Screenshot
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
hours: 0,
minutes: 0,
seconds:0
}
this.hoursInput = React.createRef();
this.minutesInput= React.createRef();
this.secondsInput = React.createRef();
}
inputHandler = (e) => {
this.setState({[e.target.name]: e.target.value});
}
convertToSeconds = ( hours, minutes,seconds) => {
return seconds + minutes * 60 + hours * 60 * 60;
}
startTimer = () => {
this.timer = setInterval(this.countDown, 1000);
}
countDown = () => {
const { hours, minutes, seconds } = this.state;
let c_seconds = this.convertToSeconds(hours, minutes, seconds);
if(c_seconds) {
// seconds change
seconds ? this.setState({seconds: seconds-1}) : this.setState({seconds: 59});
// minutes change
if(c_seconds % 60 === 0 && minutes) {
this.setState({minutes: minutes -1});
}
// when only hours entered
if(!minutes && hours) {
this.setState({minutes: 59});
}
// hours change
if(c_seconds % 3600 === 0 && hours) {
this.setState({hours: hours-1});
}
} else {
clearInterval(this.timer);
}
}
stopTimer = () => {
clearInterval(this.timer);
}
resetTimer = () => {
this.setState({
hours: 0,
minutes: 0,
seconds: 0
});
this.hoursInput.current.value = 0;
this.minutesInput.current.value = 0;
this.secondsInput.current.value = 0;
}
render() {
const { hours, minutes, seconds } = this.state;
return (
<div className="App">
<h1 className="title"> (( React Countdown )) </h1>
<div className="inputGroup">
<h3>Hrs</h3>
<input ref={this.hoursInput} type="number" placeholder={0} name="hours" onChange={this.inputHandler} />
<h3>Min</h3>
<input ref={this.minutesInput} type="number" placeholder={0} name="minutes" onChange={this.inputHandler} />
<h3>Sec</h3>
<input ref={this.secondsInput} type="number" placeholder={0} name="seconds" onChange={this.inputHandler} />
</div>
<div>
<button onClick={this.startTimer} className="start">start</button>
<button onClick={this.stopTimer} className="stop">stop</button>
<button onClick={this.resetTimer} className="reset">reset</button>
</div>
<h1> Timer {hours}: {minutes} : {seconds} </h1>
</div>
);
}
}
export default App;
I had the same problem and I found this npm package for a countdown.
install the package
npm install react-countdown --save
or
yarn add react-countdown
import the package to your file
import Countdown from 'react-countdown';
call the imported "Countdown" inside a render method and pass a date
<Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}>
or
<Countdown date={new Date("Sat Sep 26 2021")}>
Here is an example for you.
import React from "react";
import ReactDOM from "react-dom";
import Countdown from "react-countdown";
// Random component
const Completionist = () => <span>You are good to go!</span>;
ReactDOM.render(
<Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}>
<Completionist />
</Countdown>,
document.getElementById("root")
);
you can see the detailed document here https://www.npmjs.com/package/react-countdown
functionality :
1)Start
2)Reset
functional component
import {useState, useCallback} from 'react';
const defaultCount = 10;
const intervalGap = 300;
const Counter = () => {
const [timerCount, setTimerCount] = useState(defaultCount);
const startTimerWrapper = useCallback((func)=>{
let timeInterval: NodeJS.Timer;
return () => {
if(timeInterval) {
clearInterval(timeInterval)
}
setTimerCount(defaultCount)
timeInterval = setInterval(() => {
func(timeInterval)
}, intervalGap)
}
}, [])
const timer = useCallback(startTimerWrapper((intervalfn: NodeJS.Timeout) => {
setTimerCount((val) => {
if(val === 0 ) {
clearInterval(intervalfn);
return val
}
return val - 1
})
}), [])
return <>
<div> Counter App</div>
<div> <button onClick={timer}>Start/Reset</button></div>
<div> {timerCount}</div>
</>
}
export default Counter;
When you are using functional components the above code is a good option to do it:
import React, { useState, useEffect } from "react";
import { MessageStrip } from "#ui5/webcomponents-react";
import "./Timer.scss";
const nMinuteSeconds = 60;
const nSecondInMiliseconds = 1000;
const convertMinutesToMiliseconds = (minute) =>
minute * nMinuteSeconds * nSecondInMiliseconds;
const convertMilisecondsToHour = (miliseconds) => new Date(miliseconds).toISOString().slice(11, -5);
export default function Counter({ minutes, onTimeOut }) {
let [timerCount, setTimerCount] = useState(
convertMinutesToMiliseconds(minutes)
);
let interval;
useEffect(() => {
if (interval) {
clearInterval(interval);
}
interval = setInterval(() => {
if (timerCount === 0 && interval) {
onTimeOut();
clearInterval(interval);
}
setTimerCount((timerCount -= nSecondInMiliseconds));
}, nSecondInMiliseconds);
}, []);
return (
<>
<MessageStrip design="Information" hide-close-button>
Time left: {convertMilisecondsToHour(timerCount)}
</MessageStrip>
</>
);
}
Here's a simple implementation using a custom hook:
import * as React from 'react';
// future time from where countdown will start decreasing
const futureTime = new Date( Date.now() + 5000 ).getTime(); // adding 5 seconds
export const useCountDown = (stop = false) => {
const [time, setTime] = React.useState(
futureTime - Date.now()
);
// ref to store interval which we can clear out later
// when stop changes through parent component (component that uses this hook)
// causing the useEffect callback to trigger again
const intervalRef = React.useRef<NodeJS.Timer | null>(null);
React.useEffect(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
return;
}
const interval = intervalRef.current = setInterval(() => {
setTime(futureTime - Date.now());
}, 1000);
return () => clearInterval(interval);
}, [stop]);
return getReturnValues(time);
};
const getReturnValues = (countDown: number) => {
const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
return [days, hours, minutes, seconds];
};
Example of using this hook:
function App() {
const [timerStopped, stopTimer] = React.useState(false);
const [,hours,minutes,seconds] = useCountDown(timerStopped);
// to stop the interval
if( !timerStopped && minutes + seconds <= 0 ) {
stopTimer(true);
}
return (
<div className="App">
Time Left: {hours}:{minutes}:{seconds}
{ timerStopped ? (
<h1>Time's up</h1>
) : null }
</div>
);
}
A simple 24-hour countdown that can easily be customized to fit different scenarios
setInterval(function time() {
let d = new Date();
let hours = 24 - d.getHours();
let min = 60 - d.getMinutes();
if ((min + "").length == 1) {
min = "0" + min;
}
let sec = 60 - d.getSeconds();
if ((sec + "").length == 1) {
sec = "0" + sec;
}
setState(hours + ":" + min + ":" + sec);
}, 1000);
The one downside with setInterval is that it can slow down the main thread. You can do a countdown timer using requestAnimationFrame instead to prevent this. For example, this is my generic countdown timer component:
class Timer extends Component {
constructor(props) {
super(props)
// here, getTimeRemaining is a helper function that returns an
// object with { total, seconds, minutes, hours, days }
this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
}
// Wait until the component has mounted to start the animation frame
componentDidMount() {
this.start()
}
// Clean up by cancelling any animation frame previously scheduled
componentWillUnmount() {
this.stop()
}
start = () => {
this.frameId = requestAnimationFrame(this.tick)
}
tick = () => {
const timeLeft = getTimeRemaining(this.props.expiresAt)
if (timeLeft.total <= 0) {
this.stop()
// ...any other actions to do on expiration
} else {
this.setState(
{ timeLeft },
() => this.frameId = requestAnimationFrame(this.tick)
)
}
}
stop = () => {
cancelAnimationFrame(this.frameId)
}
render() {...}
}
Here is a TypeScript version of CountDown Timer in React. I used code of brother Masood and M.Georgiev
import React, {useState, useEffect, useCallback} from "react";
const Minute_to_Seconds = 60;
const Seconds_to_milliseconds = 1000;
export interface CounterProps {
minutes:number,
statusAlert: (status: string)=>void,
}
export interface TimerProps {
initialMinute: number,
initialSeconds: number,
}
const Counter: React.FC<CounterProps> = (props) => {
const convert_Minutes_To_MiliSeconds = (minute:number) => {
return minute * Minute_to_Seconds * Seconds_to_milliseconds;
}
const convert_Mili_Seconds_To_Hour = (miliseconds:number) => {
return new Date(miliseconds).toISOString().slice(11, -5);
}
const convert_Mili_Seconds_To_Minute = (miliseconds:number) => {
return new Date(miliseconds).toISOString().slice(11, -5);
}
const [timer_State, setTimer_State]=useState(0);
const [timerCount, setTimerCount] = useState(convert_Minutes_To_MiliSeconds(props.minutes));
useEffect(() => {
if (timerCount > 0) {
const interval = setInterval(() => {
if (timer_State === 0) {
props.statusAlert("start");
setTimer_State(1);
}
let tempTimerCount = timerCount;
tempTimerCount -= Seconds_to_milliseconds;
setTimerCount(tempTimerCount);
},
(timer_State === 0)
? 0
: Seconds_to_milliseconds
);
return () => {
clearInterval(interval);
}
}
else{
props.statusAlert("end");
}
}, [
timer_State,
timerCount,
props,
]);
return (
<p>
Time left: {convert_Mili_Seconds_To_Hour(timerCount)}
</p>
);
}
const Timer: React.FC<TimerProps> = (props) => {
const [ minutes, setMinutes ] = useState(props.initialMinute);
const [seconds, setSeconds ] = useState(props.initialSeconds);
useEffect(()=>{
const myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval)
} else {
setMinutes(minutes - 1);
setSeconds(59);
}
}
}, 1000)
return ()=> {
clearInterval(myInterval);
};
});
return (
<div>
{ ((minutes === 0) && (seconds === 0))
? "Press F5 to Refresh"
: <h1> {minutes}:{seconds < 10 ? `0${seconds}` : seconds}</h1>
}
</div>
)
}
const RCTAPP=()=> {
const status_Alert2=(status: string)=> {
console.log("__________________________==================== status: ", status);
if (status==="start"){
alert("Timer started");
}
else{
alert("Time's up");
}
}
return (
<div style={{textAlign: "center"}}>
<Counter
minutes={1}
// minutes={0.1}
statusAlert={status_Alert2}
/>
<Timer
initialMinute={0}
initialSeconds={30}
/>
</div>
);
}
export default RCTAPP;
Typescript/Hooks/Shorter version of #Masood's answer
import { useState, useEffect } from 'react';
type Props = {
initMin: number,
initSecs: number
};
const Timer = ({initMins, initSecs}: Props) => {
// Combining useState values together for brevity
const [ [mins, secs], setCountdown ] = useState([initMins, initSecs]);
/**
* Triggers each second and whenever mins/seconds updates itself.
*/
useEffect(() => {
// Timer that decrements itself each second and updates the mins/seconds downwards
let timerInterval = setInterval(() => {
// Countdown timer up, clear timer and do nothing
if (mins === 0 && secs === 0) {
clearInterval(timerInterval);
} else if (secs === 0) {
// Might be correct to set seconds to 59, but not sure
// should decrement from 60 seconds yeah?
setCountdown([mins - 1, 60]);
} else {
setCountdown([mins, secs - 1]);
}
}, 1000);
return () => {
clearInterval(timerInterval);
};
}, [mins, secs]);
return (
<div>
{ mins === 0 && secs === 0
? null
: <h1> {mins}:{secs < 10 ? `0${secs}` : secs}</h1>
}
</div>
)
}
export default Timer;
As we don't want the timer at the highest priority than other states so we will use useTransition hook. delay is the time in seconds 180s = 3min.
import React, { useState, useEffect, useTransition } from "react";
const Timer = ({ delayResend = "180" }) => {
const [delay, setDelay] = useState(+delayResend);
const [minutes, setMinutes] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isPending, startTransition] = useTransition();
useEffect(() => {
const timer = setInterval(() => {
startTransition(() => {
setDelay(delay - 1);
setMinutes(Math.floor(delay / 60));
setSeconds(Math.floor(delay % 60));
});
}, 1000);
if (delay === 0) {
clearInterval(timer);
}
return () => {
clearInterval(timer);
};
});
return (
<>
<span>
{minutes}:{seconds}
</span>
</>
);
};
export default Timer;

Handling methods in order in React

Hi im trying to code a Timer. My problem is that the countdown should start after clicking a button and when that status is changed from start to stop and vise versa. I cant figure out how to handle it and which level to put it.
ADDITIONAL INFO:
When clicking the button, it goes to the method handler. Changes the status with setstate() but it renders at the end. Which is to late for the countdown to start.
Here is the Game Component:
import React, { Component } from 'react';
import './Game.css';
import Timer from './Timer'
class Game extends Component {
constructor() {
super();
}
state = {
buttonStatus: {status:"Start" , classButton:"Button ButtonBackgroundColorGrey" },
dotclass : "",
timer: 60
}
componentDidMount() {
this.timersignal();
}
buttonclick = () =>{
(this.state.buttonStatus.status === "Start")
? this.setState({buttonStatus:{status:"Stop",classButton:"Button ButtonBackgroundColorRed"},dotclass:"Dot"})
: this.setState({buttonStatus:{status:"Start",classButton:"Button ButtonBackgroundColorGrey"},dotclass:""})
this.componentDidMount();
}
timersignal = () => {
if(this.state.buttonStatus.status === "Stop") {
this.Interval = setInterval(() =>{
this.setState(() => ({
timer : this.state.timer - 1
}))
},1000)
console.log("didMount start")
}
else(
console.log("didMount stop")
)
}
render() {
return (
<div>
<div className="Body-Container">
<h2 className="States"> Time </h2>
<Timer buttonstate= {this.state.timer}/>
<button className={this.state.buttonStatus.classButton} onClick={this.buttonclick}>{this.state.buttonStatus.status}</button>
</div>
</div>
);
}
}
export default Game;
Here is the Timer Component:
import React, { Component } from 'react';
import "./Timer.css";
class Timer extends Component {
render() {
return (
<h3 className="Timer">{this.props.buttonstate}</h3>
);
}
}
export default Timer ;
You just need one method and call it in componentDidMount and on click.
timerToggle = () =>{
if((this.state.buttonStatus.status === "Start") {
this.setState({buttonStatus:{status:"Stop",classButton:"Button ButtonBackgroundColorRed"},dotclass:"Dot"})
clearInterval(this.Interval);
}else{
this.setState({buttonStatus:{status:"Start",classButton:"Button ButtonBackgroundColorGrey"},dotclass:""}) ;
this.Interval = setInterval(() =>{
this.setState(() => ({
timer : this.state.timer - 1
}))
},1000)
}
}
componentDidMount() {
this.timerToggle();
}
The Final Answer:
timerToggle = () =>{
if(this.state.buttonStatus.status === "Start") {
this.setState({buttonStatus:{status:"Stop",classButton:"Button ButtonBackgroundColorRed"},dotclass:"Dot"})
this.Interval = setInterval(() =>{
this.setState(() => ({
timer : this.state.timer - 1
}))
},1000)
}
else{
this.setState({buttonStatus:{status:"Start",classButton:"Button ButtonBackgroundColorGrey"},dotclass:""}) ;
clearInterval(this.Interval);
}
}

React Stopwatch Timer counting up - not resetting the seconds [duplicate]

I have seen lots of countdown timers in JavaScript and wanted to get one working in React.
I have borrowed this function I found online:
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
};
And then I have written this code myself
initiateTimer = () => {
let timeLeftVar = this.secondsToTime(60);
this.setState({ timeLeft: timeLeftVar })
};
startTimer = () => {
let interval = setInterval(this.timer, 1000);
this.setState({ interval: interval });
};
timer = () => {
if (this.state.timeLeft >0){
this.setState({ timeLeft: this.state.timeLeft -1 });
}
else {
clearInterval(this.state.interval);
//this.postToSlack();
}
};
Currently onclick it will set the time on screen to: Time Remaining: 1 m : 0 s
But it does not reduce it to Time Remaining: 0 m : 59 s and then Time Remaining: 0 m : 58 s etc etc
I think I need to call the function again with a different parameter. how can I go about doing this ?
Edit: I forgot to say, I would like the functionality so that I can use seconds to minutes & seconds
You have to setState every second with the seconds remaining (every time the interval is called). Here's an example:
class Example extends React.Component {
constructor() {
super();
this.state = { time: {}, seconds: 5 };
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
}
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
componentDidMount() {
let timeLeftVar = this.secondsToTime(this.state.seconds);
this.setState({ time: timeLeftVar });
}
startTimer() {
if (this.timer == 0 && this.state.seconds > 0) {
this.timer = setInterval(this.countDown, 1000);
}
}
countDown() {
// Remove one second, set state so a re-render happens.
let seconds = this.state.seconds - 1;
this.setState({
time: this.secondsToTime(seconds),
seconds: seconds,
});
// Check if we're at zero.
if (seconds == 0) {
clearInterval(this.timer);
}
}
render() {
return(
<div>
<button onClick={this.startTimer}>Start</button>
m: {this.state.time.m} s: {this.state.time.s}
</div>
);
}
}
ReactDOM.render(<Example/>, document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>
Here is a solution using hooks, Timer component, I'm replicating same logic above with hooks
import React from 'react'
import { useState, useEffect } from 'react';
const Timer = (props:any) => {
const {initialMinute = 0,initialSeconds = 0} = props;
const [ minutes, setMinutes ] = useState(initialMinute);
const [seconds, setSeconds ] = useState(initialSeconds);
useEffect(()=>{
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval)
} else {
setMinutes(minutes - 1);
setSeconds(59);
}
}
}, 1000)
return ()=> {
clearInterval(myInterval);
};
});
return (
<div>
{ minutes === 0 && seconds === 0
? null
: <h1> {minutes}:{seconds < 10 ? `0${seconds}` : seconds}</h1>
}
</div>
)
}
export default Timer;
Here is a simple implementation using hooks and useInterval implementation of #dan-abramov
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
const INITIAL_COUNT = 120
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(INITIAL_COUNT)
const [status, setStatus] = useState(STATUS.STOPPED)
const secondsToDisplay = secondsRemaining % 60
const minutesRemaining = (secondsRemaining - secondsToDisplay) / 60
const minutesToDisplay = minutesRemaining % 60
const hoursToDisplay = (minutesRemaining - minutesToDisplay) / 60
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleReset = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(INITIAL_COUNT)
}
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={handleReset} type="button">
Reset
</button>
<div style={{padding: 20}}>
{twoDigits(hoursToDisplay)}:{twoDigits(minutesToDisplay)}:
{twoDigits(secondsToDisplay)}
</div>
<div>Status: {status}</div>
</div>
)
}
// 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])
}
// https://stackoverflow.com/a/2998874/1673761
const twoDigits = (num) => String(num).padStart(2, '0')
Here is the codesandbox implementation: https://codesandbox.io/s/react-countdown-demo-gtr4u?file=/src/App.js
Basic idea showing counting down using Date.now() instead of subtracting one which will drift over time.
class Example extends React.Component {
constructor() {
super();
this.state = {
time: {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
},
duration: 2 * 60 * 1000,
timer: null
};
this.startTimer = this.start.bind(this);
}
msToTime(duration) {
let milliseconds = parseInt((duration % 1000));
let seconds = Math.floor((duration / 1000) % 60);
let minutes = Math.floor((duration / (1000 * 60)) % 60);
let hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
milliseconds = milliseconds.toString().padStart(3, '0');
return {
hours,
minutes,
seconds,
milliseconds
};
}
componentDidMount() {}
start() {
if (!this.state.timer) {
this.state.startTime = Date.now();
this.timer = window.setInterval(() => this.run(), 10);
}
}
run() {
const diff = Date.now() - this.state.startTime;
// If you want to count up
// this.setState(() => ({
// time: this.msToTime(diff)
// }));
// count down
let remaining = this.state.duration - diff;
if (remaining < 0) {
remaining = 0;
}
this.setState(() => ({
time: this.msToTime(remaining)
}));
if (remaining === 0) {
window.clearTimeout(this.timer);
this.timer = null;
}
}
render() {
return ( <
div >
<
button onClick = {
this.startTimer
} > Start < /button> {
this.state.time.hours
}: {
this.state.time.minutes
}: {
this.state.time.seconds
}. {
this.state.time.milliseconds
}:
<
/div>
);
}
}
ReactDOM.render( < Example / > , document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>
simple resolution:
import React, { useState, useEffect } from "react";
const Timer = ({ delayResend = "180" }) => {
const [delay, setDelay] = useState(+delayResend);
const minutes = Math.floor(delay / 60);
const seconds = Math.floor(delay % 60);
useEffect(() => {
const timer = setInterval(() => {
setDelay(delay - 1);
}, 1000);
if (delay === 0) {
clearInterval(timer);
}
return () => {
clearInterval(timer);
};
});
return (
<>
<span>
{minutes}:{seconds}
</span>
</>
);
};
export default Timer;
The problem is in your "this" value.
Timer function cannot access the "state" prop because run in a different context. I suggest you to do something like this:
...
startTimer = () => {
let interval = setInterval(this.timer.bind(this), 1000);
this.setState({ interval });
};
As you can see I've added a "bind" method to your timer function. This allows the timer, when called, to access the same "this" of your react component (This is the primary problem/improvement when working with javascript in general).
Another option is to use another arrow function:
startTimer = () => {
let interval = setInterval(() => this.timer(), 1000);
this.setState({ interval });
};
Countdown of user input
Interface Screenshot
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor() {
super();
this.state = {
hours: 0,
minutes: 0,
seconds:0
}
this.hoursInput = React.createRef();
this.minutesInput= React.createRef();
this.secondsInput = React.createRef();
}
inputHandler = (e) => {
this.setState({[e.target.name]: e.target.value});
}
convertToSeconds = ( hours, minutes,seconds) => {
return seconds + minutes * 60 + hours * 60 * 60;
}
startTimer = () => {
this.timer = setInterval(this.countDown, 1000);
}
countDown = () => {
const { hours, minutes, seconds } = this.state;
let c_seconds = this.convertToSeconds(hours, minutes, seconds);
if(c_seconds) {
// seconds change
seconds ? this.setState({seconds: seconds-1}) : this.setState({seconds: 59});
// minutes change
if(c_seconds % 60 === 0 && minutes) {
this.setState({minutes: minutes -1});
}
// when only hours entered
if(!minutes && hours) {
this.setState({minutes: 59});
}
// hours change
if(c_seconds % 3600 === 0 && hours) {
this.setState({hours: hours-1});
}
} else {
clearInterval(this.timer);
}
}
stopTimer = () => {
clearInterval(this.timer);
}
resetTimer = () => {
this.setState({
hours: 0,
minutes: 0,
seconds: 0
});
this.hoursInput.current.value = 0;
this.minutesInput.current.value = 0;
this.secondsInput.current.value = 0;
}
render() {
const { hours, minutes, seconds } = this.state;
return (
<div className="App">
<h1 className="title"> (( React Countdown )) </h1>
<div className="inputGroup">
<h3>Hrs</h3>
<input ref={this.hoursInput} type="number" placeholder={0} name="hours" onChange={this.inputHandler} />
<h3>Min</h3>
<input ref={this.minutesInput} type="number" placeholder={0} name="minutes" onChange={this.inputHandler} />
<h3>Sec</h3>
<input ref={this.secondsInput} type="number" placeholder={0} name="seconds" onChange={this.inputHandler} />
</div>
<div>
<button onClick={this.startTimer} className="start">start</button>
<button onClick={this.stopTimer} className="stop">stop</button>
<button onClick={this.resetTimer} className="reset">reset</button>
</div>
<h1> Timer {hours}: {minutes} : {seconds} </h1>
</div>
);
}
}
export default App;
I had the same problem and I found this npm package for a countdown.
install the package
npm install react-countdown --save
or
yarn add react-countdown
import the package to your file
import Countdown from 'react-countdown';
call the imported "Countdown" inside a render method and pass a date
<Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}>
or
<Countdown date={new Date("Sat Sep 26 2021")}>
Here is an example for you.
import React from "react";
import ReactDOM from "react-dom";
import Countdown from "react-countdown";
// Random component
const Completionist = () => <span>You are good to go!</span>;
ReactDOM.render(
<Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}>
<Completionist />
</Countdown>,
document.getElementById("root")
);
you can see the detailed document here https://www.npmjs.com/package/react-countdown
functionality :
1)Start
2)Reset
functional component
import {useState, useCallback} from 'react';
const defaultCount = 10;
const intervalGap = 300;
const Counter = () => {
const [timerCount, setTimerCount] = useState(defaultCount);
const startTimerWrapper = useCallback((func)=>{
let timeInterval: NodeJS.Timer;
return () => {
if(timeInterval) {
clearInterval(timeInterval)
}
setTimerCount(defaultCount)
timeInterval = setInterval(() => {
func(timeInterval)
}, intervalGap)
}
}, [])
const timer = useCallback(startTimerWrapper((intervalfn: NodeJS.Timeout) => {
setTimerCount((val) => {
if(val === 0 ) {
clearInterval(intervalfn);
return val
}
return val - 1
})
}), [])
return <>
<div> Counter App</div>
<div> <button onClick={timer}>Start/Reset</button></div>
<div> {timerCount}</div>
</>
}
export default Counter;
When you are using functional components the above code is a good option to do it:
import React, { useState, useEffect } from "react";
import { MessageStrip } from "#ui5/webcomponents-react";
import "./Timer.scss";
const nMinuteSeconds = 60;
const nSecondInMiliseconds = 1000;
const convertMinutesToMiliseconds = (minute) =>
minute * nMinuteSeconds * nSecondInMiliseconds;
const convertMilisecondsToHour = (miliseconds) => new Date(miliseconds).toISOString().slice(11, -5);
export default function Counter({ minutes, onTimeOut }) {
let [timerCount, setTimerCount] = useState(
convertMinutesToMiliseconds(minutes)
);
let interval;
useEffect(() => {
if (interval) {
clearInterval(interval);
}
interval = setInterval(() => {
if (timerCount === 0 && interval) {
onTimeOut();
clearInterval(interval);
}
setTimerCount((timerCount -= nSecondInMiliseconds));
}, nSecondInMiliseconds);
}, []);
return (
<>
<MessageStrip design="Information" hide-close-button>
Time left: {convertMilisecondsToHour(timerCount)}
</MessageStrip>
</>
);
}
Here's a simple implementation using a custom hook:
import * as React from 'react';
// future time from where countdown will start decreasing
const futureTime = new Date( Date.now() + 5000 ).getTime(); // adding 5 seconds
export const useCountDown = (stop = false) => {
const [time, setTime] = React.useState(
futureTime - Date.now()
);
// ref to store interval which we can clear out later
// when stop changes through parent component (component that uses this hook)
// causing the useEffect callback to trigger again
const intervalRef = React.useRef<NodeJS.Timer | null>(null);
React.useEffect(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
return;
}
const interval = intervalRef.current = setInterval(() => {
setTime(futureTime - Date.now());
}, 1000);
return () => clearInterval(interval);
}, [stop]);
return getReturnValues(time);
};
const getReturnValues = (countDown: number) => {
const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
return [days, hours, minutes, seconds];
};
Example of using this hook:
function App() {
const [timerStopped, stopTimer] = React.useState(false);
const [,hours,minutes,seconds] = useCountDown(timerStopped);
// to stop the interval
if( !timerStopped && minutes + seconds <= 0 ) {
stopTimer(true);
}
return (
<div className="App">
Time Left: {hours}:{minutes}:{seconds}
{ timerStopped ? (
<h1>Time's up</h1>
) : null }
</div>
);
}
A simple 24-hour countdown that can easily be customized to fit different scenarios
setInterval(function time() {
let d = new Date();
let hours = 24 - d.getHours();
let min = 60 - d.getMinutes();
if ((min + "").length == 1) {
min = "0" + min;
}
let sec = 60 - d.getSeconds();
if ((sec + "").length == 1) {
sec = "0" + sec;
}
setState(hours + ":" + min + ":" + sec);
}, 1000);
The one downside with setInterval is that it can slow down the main thread. You can do a countdown timer using requestAnimationFrame instead to prevent this. For example, this is my generic countdown timer component:
class Timer extends Component {
constructor(props) {
super(props)
// here, getTimeRemaining is a helper function that returns an
// object with { total, seconds, minutes, hours, days }
this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
}
// Wait until the component has mounted to start the animation frame
componentDidMount() {
this.start()
}
// Clean up by cancelling any animation frame previously scheduled
componentWillUnmount() {
this.stop()
}
start = () => {
this.frameId = requestAnimationFrame(this.tick)
}
tick = () => {
const timeLeft = getTimeRemaining(this.props.expiresAt)
if (timeLeft.total <= 0) {
this.stop()
// ...any other actions to do on expiration
} else {
this.setState(
{ timeLeft },
() => this.frameId = requestAnimationFrame(this.tick)
)
}
}
stop = () => {
cancelAnimationFrame(this.frameId)
}
render() {...}
}
Here is a TypeScript version of CountDown Timer in React. I used code of brother Masood and M.Georgiev
import React, {useState, useEffect, useCallback} from "react";
const Minute_to_Seconds = 60;
const Seconds_to_milliseconds = 1000;
export interface CounterProps {
minutes:number,
statusAlert: (status: string)=>void,
}
export interface TimerProps {
initialMinute: number,
initialSeconds: number,
}
const Counter: React.FC<CounterProps> = (props) => {
const convert_Minutes_To_MiliSeconds = (minute:number) => {
return minute * Minute_to_Seconds * Seconds_to_milliseconds;
}
const convert_Mili_Seconds_To_Hour = (miliseconds:number) => {
return new Date(miliseconds).toISOString().slice(11, -5);
}
const convert_Mili_Seconds_To_Minute = (miliseconds:number) => {
return new Date(miliseconds).toISOString().slice(11, -5);
}
const [timer_State, setTimer_State]=useState(0);
const [timerCount, setTimerCount] = useState(convert_Minutes_To_MiliSeconds(props.minutes));
useEffect(() => {
if (timerCount > 0) {
const interval = setInterval(() => {
if (timer_State === 0) {
props.statusAlert("start");
setTimer_State(1);
}
let tempTimerCount = timerCount;
tempTimerCount -= Seconds_to_milliseconds;
setTimerCount(tempTimerCount);
},
(timer_State === 0)
? 0
: Seconds_to_milliseconds
);
return () => {
clearInterval(interval);
}
}
else{
props.statusAlert("end");
}
}, [
timer_State,
timerCount,
props,
]);
return (
<p>
Time left: {convert_Mili_Seconds_To_Hour(timerCount)}
</p>
);
}
const Timer: React.FC<TimerProps> = (props) => {
const [ minutes, setMinutes ] = useState(props.initialMinute);
const [seconds, setSeconds ] = useState(props.initialSeconds);
useEffect(()=>{
const myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval)
} else {
setMinutes(minutes - 1);
setSeconds(59);
}
}
}, 1000)
return ()=> {
clearInterval(myInterval);
};
});
return (
<div>
{ ((minutes === 0) && (seconds === 0))
? "Press F5 to Refresh"
: <h1> {minutes}:{seconds < 10 ? `0${seconds}` : seconds}</h1>
}
</div>
)
}
const RCTAPP=()=> {
const status_Alert2=(status: string)=> {
console.log("__________________________==================== status: ", status);
if (status==="start"){
alert("Timer started");
}
else{
alert("Time's up");
}
}
return (
<div style={{textAlign: "center"}}>
<Counter
minutes={1}
// minutes={0.1}
statusAlert={status_Alert2}
/>
<Timer
initialMinute={0}
initialSeconds={30}
/>
</div>
);
}
export default RCTAPP;
Typescript/Hooks/Shorter version of #Masood's answer
import { useState, useEffect } from 'react';
type Props = {
initMin: number,
initSecs: number
};
const Timer = ({initMins, initSecs}: Props) => {
// Combining useState values together for brevity
const [ [mins, secs], setCountdown ] = useState([initMins, initSecs]);
/**
* Triggers each second and whenever mins/seconds updates itself.
*/
useEffect(() => {
// Timer that decrements itself each second and updates the mins/seconds downwards
let timerInterval = setInterval(() => {
// Countdown timer up, clear timer and do nothing
if (mins === 0 && secs === 0) {
clearInterval(timerInterval);
} else if (secs === 0) {
// Might be correct to set seconds to 59, but not sure
// should decrement from 60 seconds yeah?
setCountdown([mins - 1, 60]);
} else {
setCountdown([mins, secs - 1]);
}
}, 1000);
return () => {
clearInterval(timerInterval);
};
}, [mins, secs]);
return (
<div>
{ mins === 0 && secs === 0
? null
: <h1> {mins}:{secs < 10 ? `0${secs}` : secs}</h1>
}
</div>
)
}
export default Timer;
As we don't want the timer at the highest priority than other states so we will use useTransition hook. delay is the time in seconds 180s = 3min.
import React, { useState, useEffect, useTransition } from "react";
const Timer = ({ delayResend = "180" }) => {
const [delay, setDelay] = useState(+delayResend);
const [minutes, setMinutes] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isPending, startTransition] = useTransition();
useEffect(() => {
const timer = setInterval(() => {
startTransition(() => {
setDelay(delay - 1);
setMinutes(Math.floor(delay / 60));
setSeconds(Math.floor(delay % 60));
});
}, 1000);
if (delay === 0) {
clearInterval(timer);
}
return () => {
clearInterval(timer);
};
});
return (
<>
<span>
{minutes}:{seconds}
</span>
</>
);
};
export default Timer;

Cannot set property 'myInterval' of undefined in React

Is anyone able to help me with this error that is appearing when the start timer function is called by a button component? I originally had it set up using componentDidMount so that the timer started on its own, but wanted to eventually implement it into a button click to start
Any other advice on how to clean up them if statements would be greatly appreciated
Cannot set property 'myInterval' of undefined
class Timer extends React.Component {
state = {
seconds: 5,
stage: "breathIn",
repetition: 4,
};
changeSeconds(x) {
const { y } = this.state;
this.setState(({ y }) => ({
seconds: x,
}));
}
changeStage(x) {
const { y } = this.state;
this.setState(({ y }) => ({
stage: x,
}));
}
changeRepetitions() {
const { repetition } = this.state;
this.setState(({ repetition }) => ({
repetition: repetition - 1,
}));
}
startTimer() {
this.myInterval = setInterval(() => {
const { seconds, stage, repetition } = this.state;
if (seconds > 0) {
this.setState(({ seconds }) => ({
seconds: seconds - 1,
}));
}
if (seconds === 0) {
if (repetition > 0) {
if (stage === "breathIn") {
this.changeSeconds(7);
this.changeStage("hold");
} else if (stage === "hold") {
this.changeSeconds(8);
this.changeStage("breathOut");
} else if (stage === "breathOut") {
if (repetition === 2) {
this.setState(({ repetition }) => ({
repetition: repetition - 1,
}));
this.changeSeconds(5);
this.changeStage("breathIn");
} else if (repetition === 1) {
this.changeSeconds(0);
this.changeStage("complete");
}
}
} else {
clearInterval(this.myInterval);
}
}
}, 1000);
}
componentWillUnmount() {
clearInterval(this.myInterval);
}
render() {
const { seconds, stage, repetition } = this.state;
return (
<div>
<Centerbutton startTimer={this.startTimer} />
<h1>{repetition}</h1>
<h1>{seconds}</h1>
</div>
);
}
}
export default Timer;
<Centerbutton startTimer={this.startTimer} />
should be either
<Centerbutton startTimer={this.startTimer.bind(this)} />
or
<Centerbutton startTimer={()=>this.startTimer()} />
It's a classic JS event binding problem
Just place your setInterval function inside componentDidMount life cycle method, follow the below example, you can't use the setInterval direct in react.
componentDidMount() {
this.interval = setInterval(() => {
// Your code here
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
Learn more about setInterval and Here is the demo app

Clear Interval setted on state

I'm setting an interval on componentDidMount() and saving it on my App state.
The timer starts correctly, but I don't achieve to clear it when I want. This is the initial state:
this.initialState = {
score: 0,
finished: false,
currentQuestion: 0,
questions: [],
minutes: 1,
seconds: 15,
timer: 0
};
The action looks like this. It receives a timer (interval) as a parameter:
export function updateTimer(timer) {
return {
type: UPDATETIMER,
payload: {
timer
}
}
}
I have this action creator function on my reducers.js:
function timer(state = 0, action = {}) {
switch(action.type) {
case RESTART:
return 0;
case UPDATETIMER:
if(action.payload.timer !== undefined){
return action.payload.timer;
}else {
clearInterval(state);
return 0;
}
default:
return state;
}
}
In App.js I use it like this:
<Navbar
onUpdateTimer= {
(timer) => {
this.props.dispatch(updateTimer(timer))
}
}
/>
What I'm tryng to do is that if I the call has an argument it sets the interval, and if it doesn't it clears it.
The component that calls the action is this one:
import React from 'react';
export default class CountDown extends React.Component {
constructor(props) {
super(props);
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
}
startTimer() {
this.props.onUpdateTimer(setInterval(this.countDown, 1000));
}
countDown() {
let minutes = this.props.minutes;
let seconds = this.props.seconds - 1;
if (seconds === 0) {
if(minutes !== 0){
minutes -=1;
seconds = 59;
}else {
this.props.timeUp()
setTimeout(function(){
alert("SE HA ACABADO EL TIEMPO");
},0);
}
}
this.props.onUpdateTime(minutes, seconds);
}
render() {
return (
<div id="clockdiv">
<span className="minutes">{this.props.minutes} : </span>
<span className="seconds">{this.props.seconds}</span>
</div>
);
}
componentDidMount(){
this.startTimer();
}
}
Am I using correctly clearInterval(state) in the action creator function?

Categories

Resources