How to not let a timer stop in nextJS when idle - javascript

Basically, I created a simple timer app with Next
https://sitstandtimer.vercel.app/
and I want it to just stay counting even if I'm not on the browser window. When I go to another window for about 5 minutes it stops counting, which is odd behavior. Here's the code for the Timer:
const useTimer = (initialState = 0) => {
const [timer, setTimer] = useState(initialState)
const [isActive, setIsActive] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const countRef = useRef(null)
const handleStart = () => {
setIsActive(true)
setIsPaused(true)
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handlePause = () => {
clearInterval(countRef.current)
setIsPaused(false)
}
const handleResume = () => {
setIsPaused(true)
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handleReset = () => {
clearInterval(countRef.current)
setIsActive(false)
setIsPaused(false)
setTimer(0)
}
return { timer, isActive, isPaused, handleStart, handlePause, handleResume, handleReset, setTimer }
}

As already answered in the comment. Setting time elapsed instead of adding 1 is usually a better solution because browsers can slow down or disable intervals in the background.
const useTimer = (initialTime = 0) => {
const [startTime, setStartTime) = useState(0);
const [timer, setTimer] = useState(initialState)
const [isActive, setIsActive] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const countRef = useRef(null)
const startInterval = () => setInterval(() => {
setTimer((Date.now() - startTime) / 1000)
}, 1000)
}
const handleStart = () => {
setStartTime(Date.now())
setIsActive(true)
setIsPaused(true)
countRef.current = startInterval()
const handlePause = () => {
clearInterval(countRef.current)
setIsPaused(false)
}
const handleResume = () => {
setIsPaused(true)
countRef.current = startInterval()
}
const handleReset = () => {
clearInterval(countRef.current)
setIsActive(false)
setIsPaused(false)
setTimer(0)
}
return { timer, isActive, isPaused, handleStart, handlePause, handleResume, handleReset, setTimer }
}

Related

setTimeout in useEffect hook

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);
});

React: ClearInterval and Immediately Start Again

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);
};
}, []);

Start interval after time delay and stop on button release / React

I am trying to build a simple plus/minus-control in React. When clicked on either plus or minus (triggered by onMouseDown) the value should change by a defined step and when the button is held the value should in-/decrease at a specified interval after a specified delay. When the button is released (onMouseUp), the interval should stop.
The code below runs ok on onMouseDown and hold, but when I just click on the button the interval starts anyway. I see that I need to make sure that the button is still down before the interval is started, but how do I achieve that? Thank you for any insights.
let plusTimer = useRef(null);
const increment = () => {
setMyValue(prev => prev + myStep);
setTimeout(() => {
plusTimer.current = setInterval(
() => setMyValue(prev => prev + myStep),
100
);
}, 500);
};
const intervalClear = () => {
clearInterval(plusTimer.current);
};
I think I will let the code speak for itself:
const {useCallback, useEffect, useState} = React;
const CASCADE_DELAY_MS = 1000;
const CASCADE_INTERVAL_MS = 100;
function useDelayedCascadeUpdate(intervalTime, delay, step, callback) {
const [started, setStarted] = useState(false);
const [running, setRunning] = useState(false);
const update = useCallback(() => callback((count) => count + step), [
callback,
step
]);
const handler = useCallback(() => {
update();
setStarted(true);
}, [update, setStarted]);
const reset = useCallback(() => {
setStarted(false);
setRunning(false);
}, [setStarted, setRunning]);
useEffect(() => {
if (started) {
const handler = setTimeout(() => setRunning(true), delay);
return () => {
clearTimeout(handler);
};
}
}, [started, setRunning, delay]);
useEffect(() => {
if (running) {
const handler = setInterval(update, intervalTime);
return () => {
clearInterval(handler);
};
}
}, [running, update, intervalTime]);
return [handler, reset];
}
function App() {
const [count, setCount] = useState(0);
const [incrementHandler, incrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
1,
setCount
);
const [decrementHandler, decrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
-1,
setCount
);
return (
<div>
<div>{count}</div>
<button onMouseDown={incrementHandler} onMouseUp={incrementReset}>
+
</button>
<button onMouseDown={decrementHandler} onMouseUp={decrementReset}>
-
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

How to set "interval id" to state in React

I'm trying to create an app that allow to create a list of timers. Each timer may be paused and resumed.
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 1 });
useEffect(() => {
const tick = () => {
const duration = moment.duration(
moment.duration((moment().format('X') - data.unix) * 1000, 'milliseconds') + 1000,
'milliseconds'
);
setTime({ hours: duration.hours(), minutes: duration.minutes(), seconds: duration.seconds() });
};
const timer = setInterval(tick, 1000);
return () => {
clearInterval(timer);
};
}, [data.unix]);
That's all I have for now and I'm trying to make a pause option that I think could be implemented by clearing interval to stop the timer. But timer is in useEffect scope and can't be accessed to delete outside useEffect. If i put timer in setState it causes error. Any advices?
in order to keep reference to the interval outside of useEffect you should use useRef
this will allow clearing the interval on an event handler like: onClick for example.
const IntervalComponent = (props) => {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = getInterval();
return () => clearInterval(intervalRef.current);
}, []);
const getInterval = () => {
const startTime = new Date().getTime();
const progressInterval = setInterval(() => {
// do on each interval
}, 10);
return progressInterval;
};
const onClickHandler = (e) => clearInterval(intervalRef.current);
return <button onClick={onClickHandler}>Clear interval</button> ;
}
I made this kind of working example (with RN, but the logic stays). I mocked the functionality of moment library: https://snack.expo.io/#zvona/setinterval-example
Like explained in the already upvoted answer, you need to use useRef when working with useEffect and intervals.
Core functionality:
const App = () => {
let timer = useRef();
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 1 });
const [toggleLabel, setToggleLabel] = useState('Pause');
const tick = useCallback(() => {
// mock:
const duration = {
hours: () => '0'.padStart(2, '0'),
minutes: () => '0'.padStart(2, '0'),
seconds: () => (''+ Math.floor(Math.random() * 60)).padStart(2, '0'),
};
setTime({
hours: duration.hours(),
minutes: duration.minutes(),
seconds: duration.seconds(),
});
}, []);
const startTicking = () => setInterval(tick, 1e3);
const stopTicking = () => clearInterval(timer.current);
const toggleTimer = () => {
const shouldPause = (toggleLabel === 'Pause');
timer.current = shouldPause ? stopTicking() : startTicking();
setToggleLabel(shouldPause ? 'Resume' : 'Pause');
};
useEffect(() => {
timer.current = startTicking();
return () => {
stopTicking();
};
}, [tick]);
const { hours, minutes, seconds} = time;
return (
<View style={styles.container}>
<Text>{`${hours}:${minutes}:${seconds}`}</Text>
<TouchableOpacity style={styles.toggleButton} onPress={toggleTimer}><Text>{toggleLabel}</Text></TouchableOpacity>
</View>
);
};

Updated state value is not reflected inside setInterval() in React

I have the following:
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // always says "false"
}, 1000);
};
Elsewhere in the code while this timer is running I'm updating the value of isPaused:
setIsPaused(true);
But this isn't reflected in the console log, it always logs false. Is there a fix to this?
The myTimer.current never changed which means isPaused is always false inside the function.
You need to make use of useEffect to update myTimer.current every time isPaused is updated.
useEffect(() => {
function startTimer() {
myTimer.current = setInterval(() => {
console.log(isPaused);
}, 1000);
};
startTimer();
return () => clearInterval(myTimer.current); // cleanup
}, [isPaused]);
You can do something like this,
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // now updates
}, 1000);
};
useEffect(() => {
startTimer();
return () => myTimer.current != null && clearInterval(myTimer.current);
}, [isPaused]);
return (
<div>
<b>isPaused: {isPaused ? "T" : "F"}</b>
<button onClick={() => setIsPaused(!isPaused)}>Toggle</button>
</div>
);
Use Others function
use useInterval from 30secondsofcode
const Timer = props => {
const [seconds, setSeconds] = React.useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<Timer />, document.getElementById('root'));
Or, use react-useInterval package
function Counter() {
let [count, setCount] = useState(0);
const increaseCount = amount => {
setCount(count + amount);
};
useInterval(increaseCount, 1000, 5);
return <h1>{count}</h1>;
}

Categories