setInterval method is interrupted in mobile PWA - javascript

I am building a pomodoro app with React and PWA feature.
I have been created a hook that helps with the countdown
useCountdown.js
import { useEffect, useState } from "react";
export function useCountdown(initialCount) {
if (typeof initialCount !== "number") {
return console.error("You must set an initial number in seconds");
}
const [intervalId, setIntervalId] = useState(null);
const [count, setCount] = useState(initialCount);
const [isCountdownFinished, setIsCountdownFinished] = useState(false);
// handling functions
useEffect(() => {
if (count === 0) {
setIsCountdownFinished(true);
} else {
setIsCountdownFinished(false);
}
}, [count]);
const countdown = () => {
// Stop countdown when reaches 0
setCount((last) => {
if (last <= 0) {
clearInterval(intervalId);
return last;
} else return last - 1;
});
};
const startCountDown = () => {
intervalId || setIntervalId(setInterval(countdown, 1000));
};
const stopCountdown = () => {
clearInterval(intervalId);
setIntervalId(null);
};
const resetCountdown = () => {
stopCountdown();
setCount(initialCount);
};
const SECS_PER_MINUTE = 60;
return [
{
minutes: Math.floor(count / SECS_PER_MINUTE),
seconds: count % SECS_PER_MINUTE,
count,
},
setCount,
startCountDown,
stopCountdown,
resetCountdown,
isCountdownFinished,
];
}
The PWA in desktop works fine, but the problem comes when I install the app in mobile, the countdown seems to stop, I notice that It is because the browser set the app in background.
This is the app https://pomo-san.vercel.app/
I expected that it could works fine as in desktop
How can I solve that?

Related

react-native state is read-only

Following Component in react-native:
import { useEffect, useState } from 'react'
let startValue = null // only using this to restart the counter from resetTimer() (other better approaches?)
export const NewTimer = () => {
const [seconds, setSeconds] = useState(startValue)
const formatedTime = () => {
return [pad(parseInt(seconds / 60)), pad(seconds % 60)].join(':')
}
useEffect(() => {
const timer = setInterval(() => setSeconds(++seconds), 1000) // I guess this line triggers the error
return () => {
clearInterval(timer)
}
}, [])
return formatedTime
}
const pad = (num) => {
return num.toString().length > 1 ? num : `0${num}`
}
export const resetTimer = () => {
startValue = 0
}
results in Uncaught Error: "seconds" is read-only
Can anyone point where the mistake is? Thx!
when you do ++seconds, you are attempting to mutate seconds this render, which isn't allowed. I would use the setState callback to get the current value, and just do seconds + 1, which will accomplish the same thing:
useEffect(() => {
const timer = setInterval(() => setSeconds((seconds) => seconds + 1), 1000)
return () => {
clearInterval(timer)
}
}, [])

Can't get my simple react timer to work without causing endless loops

I'm trying to create a simple timer in my react app that counts up from 0 to 10 and then stops. But I can't get it to work without getting caught in an infinite loop.
import React, { useState, useEffect } from "react";
const Countdown = () => {
const [countdown, setCountdown] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCountdown(countdown + 1);
}, 1000);
if (countdown >= 10) {
return () => clearInterval(interval);
}
}, [countdown]);
return <div>{countdown}</div>;
};
export default Countdown;
You have to stop using setInterval if it is greater than 10. But you are clearing the interval if it is greater than 10 but the setCountdown still setting countdown.
But you should use setTimeout instead of setInterval and initialize the countdown to 1
CODESANDBOX
import React, { useState, useEffect } from "react";
const Countdown = () => {
const [countdown, setCountdown] = useState(1);
useEffect(() => {
let timeout;
if (countdown < 10) {
timeout = setTimeout(() => {
setCountdown(countdown + 1);
}, 1000);
}
return () => clearTimeout(timeout);
}, [countdown]);
return <div>{countdown}</div>;
};
export default Countdown;
// here is the alternative to count the process time.
console.time('looper');
for(let a=0; a<100000; a++){}
console.timeEnd('looper');

React native, call a function inside setInterval when time ==60

I created a function, when user click start button that function and timer will start. However, when time reach 60 I want to call stop function but can't figure out how to that. can someone tell me how to do that please.
const [time,setTime] = useState(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time != 60) {
setTime(prevState => prevState + 1);
if (time == 60) {
onStop()
}
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
If you don't have to use time in your JSX, then consider converting it to ref. There are certainly closure issues which will take place where the value of time state that you expect to be isn't what it will be. Your logic problem of correctly using if statements is also covered here.
const time = useRef(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time.current != 60) {
time.current+=1;
}
if (time.current == 60) {
onStop()
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
And in case you need a state to be used in JSX, just make one timer as ref and time as state like so :-
const timer = useRef(0)
const [time,setTime] = useState(0);
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (timer.current != 60) {
setTime(prevState => prevState + 1);
timer.current+=1;
}
if (timer.current == 60) {
onStop()
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
See this codesandbox example doing what you want :-
Here is another React way which is much less code and confusion :-
const [time, setTime] = useState(0);
const timeout = useRef();
useEffect(() => {
onStart();
}, []);
useEffect(() => {
if (time === 60) {
onStop();
}
}, [time]);
const onStart = () => {
timeout.current = setInterval(() => {
setTime((prevState) => prevState + 1);
}, 1000);
};
const onStop = () => {
clearInterval(timeout.current);
};
You are first checking if time != 60 this means when its 60 it will not enter in the statement and will not reach the second statement. I think you have to do it like this:
timeout.current = setInterval(() => {
if (time == 60) {
onStop()
}
setTime(prevState => prevState + 1);
}, 1000);
From the looks of it. Your code will never stop at 60. As the if statement if (time != 60) only runs the code if the time IS NOT 60. Then within that if statement, you've got another if statement going if time is 60 which it CAN'T be within that if statement as that code is only executed when time does not equal 60
Change
const [time,setTime] = useState(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time != 60) {
setTime(prevState => prevState + 1);
if (time == 60) {
onStop()
}
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
to
const [time,setTime] = useState(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time != 60) {
setTime(prevState => prevState + 1);
}
if (time === 60) {
onStop();
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}

Faced problem with interval not being cleared in React

I was doing a project in React and what I wanted to do is to start the calculation of factorial of 1000 on start button and cancel the calculation on cancel button click. Thus, I used setInterval here is the whole code:
import React, { useState } from "react";
const Button = ({ title, countButton }) => {
const [result, setResult] = useState(0);
let interval;
const handleFactorial = (num) => {
let iteration = 1;
let value = 1;
interval = setInterval(function () {
value = value * iteration;
console.log(iteration++);
if (iteration === num) {
setResult(value);
console.log(result);
clearInterval(interval);
}
}, 0);
};
let cancelFactorial = () => {
clearInterval(interval);
};
return countButton ? (
<button onClick={() => handleFactorial(1000)}>{title}</button>
) : (
<button onClick={cancelFactorial}>{title}</button>
);
};
export default Button;
The problem is when I click on cancel button which is this one <button onClick={cancelFactorial}>{title}</button> but calculation keeps going. Thus I need your help
You should use a reference for that as if you log your interval value, you will notice that you re-assign its value on every render.
const Button = ({ title, countButton }) => {
const intervalRef = useRef();
const handleFactorial = (num) => {
intervalRef.current = setInterval(function () {...}, 0);
};
let cancelFactorial = () => {
clearInterval(intervalRef.current);
};
...
}
You need to use [useRef][1] to keep a reference to your interval.
// don't do that
// let interval;
// do this instead
const intervalRef = useRef();
interval.current = setInterval(function () { ... })
const cancelFactorial = () => {
clearInterval(interval.current);
};

Prompt user before the session ends

I need to display a dialog box where it shows the remaining time for the current session to expire. I have implemented it, unfortunately,the timer is ticking multiple times.Here is my code.
warningTime and timeout value is fetched from the api which is in parent component.
const [ remainingTime, setRemainingTime ] = useState(warningTime);
useEffect(() => {
let interval = null;
if (timeout > 0) {
let sessionTimeoutInterval = setInterval(() => {
let runTime = localStorage.getItem("timeout");
if (parseInt(runTime) === warningTime) {
openDialog();
if(remainingTime===warningTime) {
interval = setInterval(() => {
if (remainingTime > 0) {
setRemainingTime(remainingTime => remainingTime - 1);
}
}, 1000);
}
if(remainingTime === 0) {
handleDialogClose();
clearInterval(interval);
}
} else{
localStorage.setItem("timeout", --runTime);
}
}, 1000);
if (remainingTime === 0) {
handleDialogClose();
handleLogout();
}
return () => {
clearInterval(sessionTimeoutInterval);
};
}
}, [timeout, remainingTime, warningTime ]);
remainingTime will be displayed in dialog.
I have made couple of change in the code.
I am using useRef to hold the status of the component. So in useEffect i am checking if the component is mounted and then setting the timer value from the localStorage or from the props value and on subsequent updates useEffect will not execute the code inside if (init.current) block.
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component
Note useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render.
I am using setTimeout to update the sessionTimeout state after every 1 second to update the timer, so the state update will execute the useEffect hook after each update as the sessionTimeout in included in the useEffect dependency.
Try this.
import React, { useEffect, useState, useRef } from "react";
import DialogBox from "./DialogBox";
import Logout from "./Logout";
export default function Child({ warningTime, timeout }) {
const [showDialog, setShowDialog] = useState(false);
const [showLogout, setShowLogout] = useState(false);
const [sessionTimeout, setSessionTimeout] = useState(timeout);
const init = useRef(true);
const progressbar = useRef(warningTime);
useEffect(() => {
if (init.current) {
init.current = false;
let sessionTime = localStorage.getItem("timeout");
if (sessionTime && sessionTime < warningTime) {
progressbar.current = sessionTime;
} else {
progressbar.current = warningTime;
}
if (sessionTime) {
setSessionTimeout(prevState => sessionTime);
} else {
localStorage.setItem("timeout", sessionTimeout);
}
}
let sessionTimeoutInterval = null;
if (sessionTimeout > 0) {
sessionTimeoutInterval = setTimeout(() => {
if (sessionTimeout <= warningTime) {
openDialog();
}
setSessionTimeout(sessionTimeout => {
let updatedtime = sessionTimeout - 1;
localStorage.setItem("timeout", updatedtime);
return updatedtime;
});
}, 1000);
} else {
localStorage.removeItem("timeout");
handleDialogClose();
handleLogout();
clearTimeout(sessionTimeoutInterval);
}
return () => {
if (sessionTimeoutInterval) clearTimeout(sessionTimeoutInterval);
};
}, [sessionTimeout]);
function openDialog() {
setShowDialog(true);
}
function handleDialogClose() {
setShowDialog(false);
}
function handleLogout() {
setShowLogout(true);
}
function addMoreTimeHandler() {
handleDialogClose();
setSessionTimeout(sessionTimeout => {
localStorage.setItem("timeout", timeout);
return timeout;
});
}
return (
<div>
{showLogout ? <Logout /> : "Time remaning: " + sessionTimeout}
{showDialog ? (
<DialogBox
progressBar={progressbar.current - 1}
sessionTimeout={sessionTimeout}
addMoreTime={addMoreTimeHandler}
/>
) : null}
</div>
);
}
Live Example
I tried to simulate your code with dummy data and it worked.
let [ remainingTime, setRemainingTime, sessionTimeout, warningTime] = [1000, 5000, 10000, 1000];
let runTime = 3000;
function abc() {
let interval = null;
if (sessionTimeout > 0) {
let sessionTimeoutInterval = setInterval(() => {
if (parseInt(runTime) === warningTime) {
// openDialog();
console.log("open dialog");
if(remainingTime===warningTime) {
interval = setInterval(() => {
if (remainingTime > 0) {
remainingTime -= 1000;
}
}, 1000);
}
if(remainingTime === 0) {
// handleDialogClose();
console.log("close dialog");
clearInterval(interval);
clearInterval(sessionTimeoutInterval);
}
} else {
if(runTime > 0){ // this condition is newly added
runTime-=1000; }
}
}, 1000);
}
}
abc();

Categories

Resources