Simple countdown - onClick button fires on render - reactjs

I'm trying to build a very simple 5 second countdown in React and can't seem to get it to work. In the code below, the timer starts as soon as the app renders for the first time, and then once it gets past 0 it seems to flicker and break down.
Any suggestions?
import './styles/App.css';
import { useState } from 'react';
function App() {
const [ time, setTime ] = useState(5);
const startCountdown = setInterval(() => {
if (time < 0 ) {
clearInterval(startCountdown);
}
setTime(time - 1);
}, 1000);
return (
<div className="App">
<h1>{time}</h1>
<button onClick={startCountdown}>Start</button>
</div>
);
}
export default App;

You need to pass it as a function. Currently your assigning this as code block which will be executed straightway.
exp:
const startCountdown = () => setInterval(() => {
if (time < 0 ) {
clearInterval(startCountdown);
}
setTime(time - 1);
}, 1000);

Related

ReactJs/NextJS - automatic redirect after 5 seconds to another page

I am creating welcome page which should appear right after customer is logged in and this page should automatically redirect customer to another page after 5 seconds. I tried to use setState with default value of 5 seconds and then using setInterval of 1000ms, decrease it by 1 and when it comes to 0, redirect customer.
Console.log() always returns 5, so basically on frontend I always see this sentence:
Welcome, you have successfully logged in, you will be redirected in.. 4
Looks like that because setRedirectSeconds() is asynchronous, it wont update state right at the moment and whenever my setInterval function starts again, redirectSeconds will still be 5 each time.
How to fix that?
Here is my code:
const Welcome: NextPage = (): ReactElement => {
const [redirectSeconds, setRedirectSeconds] = useState<number>(5);
const router = useRouter();
const query = router.query;
useEffect(() => {
if (query?.redirect) {
setTimeout(() => {
const interval = setInterval(() => {
console.log(redirectSeconds);
if (redirectSeconds > 0) {
setRedirectSeconds(redirectSeconds - 1);
} else {
clearInterval(interval);
router.push(query.redirect.toString());
}
}, 1000)
}, 1000);
}
}, []);
return (
<div>
Welcome, you have successfully logged in, you will be redirected in.. {redirectSeconds}
</div>
);
};
export default Welcome;
Here is how I adjusted it and now it works fine:
I have removed setInterval and added dependancy to useEffect function. And I have added timeout of 1000ms before decreasing redirectSeconds.
useEffect(() => {
if (query?.redirect) {
if (redirectSeconds == 0) {
router.push(query.redirect.toString());
return;
}
setTimeout(() => {
console.log(redirectSeconds);
setRedirectSeconds((redirectSeconds) => redirectSeconds - 1);
}, 1000)
}
}, [redirectSeconds]);
You can use the below custom hook to achieve this functionality.
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
export default function useRedirectAfterSomeSeconds(redirectTo, seconds = 5) {
const [secondsRemaining, setSecondsRemaining] = useState(seconds);
const router = useRouter();
useEffect(() => {
if (secondsRemaining === 0) router.push('/');
const timer = setTimeout(() => {
setSecondsRemaining((prevSecondsRemaining) => prevSecondsRemaining - 1);
if (secondsRemaining === 1) router.push(redirectTo);
}, 1000);
return () => {
clearInterval(timer);
};
}, [router, secondsRemaining, redirectTo]);
return { secondsRemaining };
}
Usage example:
import Head from 'next/head';
import useRedirectAfterSomeSeconds from '../hooks/useRedirectAfterSomeSeconds';
export default function ErrorPage() {
const { secondsRemaining } = useRedirectAfterSomeSeconds('/', 5);
return (
<>
<Head>
<title>Page not found - redirecting in 5 seconds</title>
</Head>
<main>
<h1>404</h1>
<p>
This page cannot be found. Redirecting to homepage in
{secondsRemaining} {secondsRemaining > 1 ? 'seconds' : 'second'}.
</p>
</main>
</>
);
}
Set a top level state variable. when it hits 0, redirect the user
import { useNavigate } from "react-router-dom";
const [count, setCount] = useState(5);
Inside useEffect write a setInterval to decrease count by 1 after 1000ms
const navigate = useNavigate();
useEffect(()=>{
// each second count=count-1
const interval=setTimeInterval(()=>{
setCount((updatedCount)=>updatedCcount-1)
},1000)
// if count===0 redirect
count==0 && navigate(`/${query.redirect.toString()}`)
// always clear the timeers in return function
return ()=>{
clearIntercal(interval)
}
},[count,navigate])
then write the message.
p>
This page cannot be found. Redirecting to homepage in
{count} {count > 1 ? 'seconds' : 'second'}.
</p>

React useRef() with setInterval() in useCallBack() function not clearing upon clearInterval()

I have this stopwatch code, which shows the elapsed time on screen. (React Component)
It all works okay, but calling "clearInterval(increment.current)" (which using using a useRef() for scope in React) doesn't seem to really "stop" the Interval. It keeps logging "Triggered" in the console (every second) - and I don't follow why it's still being called after clearInterval() when currentActivityOn === false. (And if the timer is triggered several times, it triggers more than once per second.)
Any suggestions?
import React, { useEffect, useRef, useCallback, useContext } from 'react'
import Container from '#material-ui/core/Container'
import Typography from '#material-ui/core/Typography'
import useLocalStorage from '../../hooks/useLocalStorage'
import { FlowTimeContext } from '../../services/flow/flow-time.context'
import { useStyles } from './grid-stopwatch.styles'
export default function GridStopwatch() {
const classes = useStyles()
const { currentActivityOn } = useContext(FlowTimeContext)
const [timerStartTime, setTimerStartTime] = useLocalStorage('timerStartTime', '')
const [timer, setTimer] = useLocalStorage('timer', 0)
const increment = useRef(null)
const handleTimerRun = useCallback(() => {
increment.current = setInterval(() => {
if(timerStartTime !== ''){
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
} else {
console.log("Triggered") // Why is this being called continually?
}
}, 1000)
}, [setTimer, timerStartTime])
const handleReset = useCallback(() => {
clearInterval(increment.current)
setTimer(0)
setTimerStartTime('')
}, [setTimer, setTimerStartTime])
const handleStart = useCallback(() => {
if(timerStartTime === ''){
var start = Math.floor(new Date().getTime() / 1000)
setTimerStartTime(start)
}
}, [timerStartTime, setTimerStartTime])
const formatTime = () => {
const getSeconds = `0${timer % 60}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours}h : ${getMinutes}m : ${getSeconds}s`
}
useEffect(() => {
if(currentActivityOn === true){
handleStart()
handleTimerRun()
} else {
handleReset()
}
}, [currentActivityOn, handleStart, handleReset, handleTimerRun])
return (
<Container maxWidth='xl' className={classes.container}>
{currentActivityOn && (
<Typography variant='subtitle2' gutterBottom>
Current Activity Duration: {formatTime()}
</Typography>
)}
</Container>
)
}
So the useEffect gets triggered twice in this case. So handleTimerRun got called twice, and it seems each time there was an additional interval started that was never cleared.
const handleTimerRun = useCallback(() => {
if(timerStartTime !== ''){
increment.current = setInterval(() => {
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
}, 1000)
}
}, [setTimer, timerStartTime, increment])
For now I'm able to work around this by moving the condition before starting the interval, so it only gets started once. This seems to solve my issue, but is a workaround rather than truly understanding "how" there can be an additional interval started that is assigned the same id. (and seemingly can't be "stopped")
Further clarity on that would certainly be appreciated.
It seems that if I console log increment.current, it keeps increasing - whereas I thought it would be a consistent ID...? (mutable yes, but consistent when not being mutated.)

Next.js text cycling component never updates

I'm trying to write what I thought would be a simple component: it takes an array of strings, and every three seconds the text in the div of the component is changed to the next item in the array, looping back to the beginning.
Although the console shows that the change message function is run every three seconds, the message never changes. I presume this is because the useState update never happens. Why is this, and how do I get it to work?
// components/textCycle.js
import { useState, useEffect} from 'react';
function TextCycle ( { array } ) {
const [ msg, setMsg ] = useState(0);
useEffect(() => {
function changeMsg() {
setMsg((msg > array.length) ? 0 : msg + 1);
}
setInterval( changeMsg, 3000);
}, []);
return (
<div>
{ array[msg] }
</div>
);
};
export default TextCycle;
// components/textCycle.js
import { useState, useEffect} from 'react';
function TextCycle ( { array } ) {
const [msg, setMsg] = useState(0);
function changeMsg() {
setMsg((msg > array.length - 2) ? 0 : msg + 1);
}
useEffect(() => {
setTimeout(changeMsg, 1000);
}, [msg]);
return (
<div>
{array[msg]}
</div>
);
};
export default TextCycle;

useEffect stops working after the first time useState's set becomes stale within a timer

I essentially want a timer to update a displayed calculated time constantly. If I run the setTimeout faster than the expected time the calculation results in a new number for the time (840ms vs 1000ms), the loop ends forever. How do I make this work with React Hooks? Not looking for the answer of just keeping it at 1000ms. I'm trying to find if my usage of useState and useEffect are incorrect or that there's a better hook that I'm not thinking of.
import { useEffect, useState } from "react";
function App() {
const [display, setDisplay] = useState("0");
useEffect(() => {
console.log("display", display);
function calculateTime() {
console.log('timer', Date.now() - 1626712121266);
return Date.now() - 1626712121266;
}
let timeoutId: NodeJS.Timeout;
timeoutId = setTimeout(() => {
console.log("setTimeout")
setDisplay(displayTime(calculateTime()));
}, 840);
return () => {
clearTimeout(timeoutId);
}
}, [display]);
return (
<>
{display}
</>
);
}
function displayTime(milliseconds: number) {
const seconds = Math.floor(milliseconds / 1000 % 60);
const displaySeconds = (seconds < 10) ? '0' + seconds : seconds;
let displayString = "" + displaySeconds;
console.log('displayString', displayString)
return displayString;
}
export default App;
Console results after a refresh. Notice how once seconds stay the same, useEffect stops being called.
How I created this test:
yarn create react-app test-timer --template typescript
Replaced the App() function with what is shown here.
Converting the useState into useReducer with setInterval as the timing function did the trick. useReducer is still like a magical black box to me but it works for now without a hitch. Would love an explanation why this is happening.
import { useEffect, useReducer } from "react";
function App() {
function reducer(state: any, action: any): any {
if (action.type === 'displayUpdate') {
const display = displayTime(Date.now() - 1626712121266);
return {
display: display,
};
}
}
const [state, dispatch] = useReducer(reducer, { display: "0" });
useEffect(() => {
console.log("useEffect");
let intervalId = setInterval(() => {
console.log("setInterval")
dispatch({type: 'displayUpdate'});
}, 500);
return () => {
console.log('useEffect return')
clearInterval(intervalId);
}
}, []);
return (
<>
{state.display}
</>
);
}
function displayTime(milliseconds: number) {
const seconds = Math.floor(milliseconds / 1000 % 60);
const displaySeconds = (seconds < 10) ? '0' + seconds : seconds;
let displayString = "" + displaySeconds;
console.log('displayString', displayString)
return displayString;
}
export default App;

Is there a reason why my clearInterval function isnt working in React?

I've created a simple timer script initialized with 10 seconds for testing. The problem I'm having is that the pause timer button isn't working as expected.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function startTimer() {
setTimerRunning(true);
interval = setInterval( function() {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
I want the pause button to pause the timer. Eventually I'll make conditional statements to have each button appear based on the state of the app and the value of time, but the pause button is my current obstacle.
I first had a separate countdown function which used a conditional to stop the time when the time matched counter (below). I thought of a less complicated way that lets me omit the counter variable (above). Im not sure which option is better, or if either is preventing the clearInterval function to work properly. The clearInterval function works within the countdown function if statement, but will not work outside of it.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let counter = 0;
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function countdown() {
counter++;
if ( counter === time ) {
setTimerRunning(false);
clearInterval(interval);
}
updateTime(previousTime => previousTime - 1);
}
function startTimer() {
setTimerRunning(true);
interval = setInterval(countdown, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
Basically you can't create let interval; and assign it a setInterval like interval = setInterval(countdown, 1000);
because on each re-render there will be new let interval;
what you need to do is create a variable which isn't change on re-redners, you can use useRef
const interval = useRef(null);
.....
function startTimer() {
interval.current = setInterval(countdown, 1000);
}
....
function pauseTimer() {
clearInterval(interval.current);
}
and I don't think you need const [timerRunning, setTimerRunning] = useState(false);
find a demo here
Basically when functional component re-renders it will execute from top to bottom, if you use like let counter = 0;, then on each re-render it will initialize to 0, if you need to persists your values in each re-renders you might need some hooks (Ex: useState, useRef ...), in this case useRef would do the trick (because you need only one setInterval in each re-renders and useRef will give you the previous value, it will not re-initalize like a general variable)
You have to use useEffect, like this:
const handleStart = () => {
setChangeValue(true)
}
const handlePause = () => {
setChangeValue(false)
pauseTimer()
}
const handleRestart = () => {
setInitialState()
setChangeValue(true)
}
useEffect(() => {
if (changeValue) {
const interval = setInterval(() => {
startTimer()
}, 100)
return () => clearInterval(interval)
}
}, [changeValue])
you have three buttons to start, pause and restart, invoke these (handleStart, handlePause, handleRestart) functions with them
that is my solution
instead of the startTime function, I use useEffect
useEffect(()=>{
interval = timerRunning && setInterval(() => {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
return ()=> clearInterval(interval)
},[timerRunning])
and in onClick Start Button
<button onClick={()=> setTimerRunning(true)}>Start</button>
I hope it is useful

Resources