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
}
Related
I am attempting to make it so that a different page from an array is displayed every 5 seconds. I currently have it working, except the page isn't always switching every 5 seconds, but sometimes 10 or 15 seconds. I believe this to be because I am not clearing the timeout correctly, any suggestions?
const pages = [
'screen1', 'screen2'
];
const [displayedPage, setDisplayedPage] = useState(pages[0]);
const [count, setCount] = useState(0);
useEffect(() => {
const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
const randomNumber = pages[Math.floor(Math.random() * pages.length)];
if (randomNumber === displayedPage) {
setCount(count + 1);
clearTimeout(timer);
return timer;
}
setDisplayedPage(randomNumber);
}, 5000);
return () => clearTimeout(timer);
});
To make everlasting cycles you should use setInterval, to avoid problems with rerenders you can useRef
const pages = [ 'screen1', 'screen2' ];
const [displayedPage, setDisplayedPage] = useState(0);
const [count, setCount] = useState(0);
const timer = useRef()
useEffect(() => {
const timer.current = setInterval(() => {
const randomNumber = Math.floor(Math.random() * pages.length);
setCount(count + 1);
setDisplayedPage(current =>
randomNumber == current ?
pages[(current+1)%pages.length]
:
pages[randomNumber]
);
}, 5000);
return () => clearInterval(timer.current);
});
I am facing a problem when the app is running the timer is working correctly however when I reset the timer the countdown timer is stuck in the initial state.
const [timer, setTimer] = useState(30)
useEffect(() => {
let interval = setInterval(() => {
setTimer(prev => {
if (prev === 1) clearInterval(interval)
return prev - 1
})
}, 1000)
newQuestion()
return () => clearInterval(interval)
}, [])
const handleReset = () => {
setTimer(30)
setScore(0)
}
I have a component that sets off a timer which updates and makes an axios request every 30 seconds. It uses a useRef which is set to update every 30 seconds as soon as a function handleStart is fired.
const countRef = useRef(null);
const lastUpdatedRef = useRef(null);
const [lastUpdated, setLastUpdated] = useState(Date.now())
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
Now I have a useEffect that runs a calculate function every 30 seconds whenever lastUpdated is triggered as a dependency:
const firstCalculate = useRef(true);
useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
This updates the calculate function every 30 seconds (00:30, 01:00, 01:30 etc.) as per lastUpdatedRef. However, I want the timer to restart from when lastUpdated state has been modified elsewhere (e.g. if lastUpdated was modified at 00:08, the next updated will be 00:38, 01:08, 01:38 etc.). Is there a way to do this?
Basically it sounds like you just need another handler to clear and restart the 30 second interval updating the lastUpdated state.
Example:
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
}
Full example:
const calculateModel = () => console.log("calculateModel");
export default function App() {
const countRef = React.useRef(null);
const lastUpdatedRef = React.useRef(null);
const [lastUpdated, setLastUpdated] = React.useState(Date.now());
const [timer, setTimer] = React.useState(0);
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const firstCalculate = React.useRef(true);
React.useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>Timer: {timer}</div>
<button type="button" onClick={handleStart}>
Start
</button>
<button type="button" onClick={handleOther}>
Other
</button>
</div>
);
}
Don't forget to clear any running intervals when the component unmounts!
React.useEffect(() => {
return () => {
clearInterval(countRef.current);
clearInterval(lastUpdatedRef.current);
};
}, []);
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>
);
};