Prompt user before the session ends - javascript

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

Related

setInterval method is interrupted in mobile PWA

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?

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

React setInterval question (using setState inside another setState problem)

I'm new to reactjs; I encountered this problem while studying about useState. I'm trying to decrease the value of the second state when the first state decreases to 0, and the iteration will run until both states are 0. But the second state always decreases by 2, which makes me confused.
This is my code:
import { useState } from "react";
import "./styles.css";
export default function App() {
const [firstCount,setFirstCount]=useState(10);
const [secondCount,setSecondCount] = useState(5);
function decreaseCount(){
const interval= setInterval(()=>{
setFirstCount((prevFirstCount)=>{
if(prevFirstCount>0){
return prevFirstCount-1;
}
else{
setSecondCount((prevSecondCount)=>{
if(prevSecondCount>0){
return prevSecondCount-1
}
else{
clearInterval(interval);
return prevFirstCount
}
})
return 10;
}
})
},1000)
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={(decreaseCount)}>Decrease Count</button>
</div>
);
}
codesandbox link: https://codesandbox.io/s/interval-setcountprob-plpzl?file=/src/App.js:0-835
I'd really appreciate if someone can help me out.
It's because the callback you pass to setFirstCount must be pure, but you violated that contract by trying to use it to mutate secondCount. You can correctly implement this dependency with useRef and useEffect:
export default function App() {
const [firstCount, setFirstCount] = useState(0);
const [secondCount, setSecondCount] = useState(6);
const firstCountRef = useRef(firstCount);
const secondCountRef = useRef(secondCount);
firstCountRef.current = firstCount;
secondCountRef.current = secondCount;
function decreaseCount() {
const interval = setInterval(() => {
if (secondCountRef.current === 0) {
clearInterval(interval);
return;
}
const { current } = firstCountRef;
setFirstCount(prev => (prev + 9) % 10);
setSecondCount(prev => current > 0 ? prev : (prev + 9) % 10);
}, 1000);
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={decreaseCount}>Decrease Count</button>
</div>
);
}
However, it might be easier to use a single state and compute the counts from that:
export default function App() {
const [count, setCount] = useState(60);
const countRef = useRef(count);
const firstCount = count % 10;
const secondCount = Math.floor(count / 10);
countRef.current = count;
function decreaseCount() {
const interval = setInterval(() => {
if (countRef.current === 0) {
clearInterval(interval);
return;
}
setCount(prev => prev - 1);
}, 1000);
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={decreaseCount}>Decrease Count</button>
</div>
);
}
I solved the issue this way :
const [firstCount, setFirstCount] = useState(10);
const [secondCount, setSecondCount] = useState(5);
const handleDecrease = () => {
setInterval(() => {
setFirstCount((prev) => {
if (prev > 0) {
return prev - 1;
}
if (prev === 0) {
return prev + 10;
}
});
}, 1000);
};
React.useEffect(() => {
if (firstCount === 0) {
setSecondCount((prev) => {
if (prev === 0) {
setFirstCount((firstPrev) => firstPrev + 10);
return prev + 5;
} else {
return prev - 1;
}
});
}
}, [firstCount]);
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={handleDecrease}>Decrease Count</button>
</div>
);
You shouldn't declare functions like this:
function decreaseCount(){
...
Instead you should use useCallback:
const decreaseCount = useCallback(() => {
//your code here
}[firstCount, secondCount]) //dependency array
You should read more about hooks: https://reactjs.org/docs/hooks-intro.html

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

Problem with useEffect and navbar active on scroll "To fix, cancel all subscriptions"

I'm trying to make active anchors in navbar navigation on scroll. Everything is working until I don't change page and return back to home page, then when I scroll page I get an error from useEffect hook " Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. " How I should cancel all subscriptions ?
useEffect code :
const [headerText, setHeader] = useState(false);
let mount = false;
useEffect(() => {
if (!mount) {
scrollActiveNav();
scrollStickyNav((header) => {
setHeader(header);
});
}
return () => {
mount = true;
};
}, []);
Sticky navbar function :
const scrollStickyNav = (cb) => {
const scrollSticky = window.addEventListener("scroll", () => {
const header = document.getElementById("navbar");
if (window.pageYOffset >= 80) {
header.classList.add("navbar-sticky");
header.classList.remove("absolute");
cb(true);
} else {
header.classList.remove("navbar-sticky");
header.classList.add("absolute");
cb(false);
}
});
return window.removeEventListener("scroll", scrollSticky);
}
Acitve link anchor in navabar function:
const scrollActiveNav = () => {
const activeNav = window.addEventListener('DOMContentLoaded', () => {
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.add('active');
} else {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.remove('active');
}
});
}, options);
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
return window.removeEventListener("DOMContentLoaded", activeNav);
}
Try change this line let mount = false; for this const mount = useRef(false).
const [headerText, setHeader] = useState(false);
let mount = useRef(false);
useEffect(() => {
if (!mount.current) {
scrollActiveNav();
scrollStickyNav((header) => {
setHeader(header);
});
mount.current = true;
}
}, []);
Did you try to do something like this?
useEffect(() => {
scrollActiveNav();
const activeNav = window.addEventListener('DOMContentLoaded', () => {
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.add('active');
} else {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.remove('active');
}
});
}, options);
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
return () => {
window.removeEventListener("DOMContentLoaded", activeNav);
};
}, []);

Categories

Resources