setTimeout and clearTimeout in React - reactjs

I'm struggling with creating a logout feature when users don't make any click event for 30 seconds on the page using setTimeout and clearTimeout.
But every time the users click anything on the page, the remaining time must be reset to 30 seconds again( or as an another option, clearTimeOut and setTimeOut will be used.) Meanwhile when the users don't click anything on the page, they are gonna be logged out automatically after 30seconds by removing the accessToken.
So to resolve the problem, my approach is like this:
setTimeout() will be begun when the users come over to this page.
when the users click something on the page, clearTimeOut will be active and setTimeOut will be active again too
when the users don't click anything on the page for 30 seconds, they are gonna be logged out automatically by removing the accessToken in the local storage
Currently making the users logged out after 30seconds by removing the accessToken works!
and setTimeOut in UseEffect works too.
The only matter is I have no idea how to make clearTimeOut() and setTimeOut work when the users make click event on the page..
import styled from 'styled-components';
import React, {useRef, useEffect, useState} from 'react';
import ScreenContainer from '../../src/component/common/ScreenContainer';
import {useNavigate as useDomNavigate} from 'react-router-dom';
import isNil from 'lodash/isNil';
import userClient from '../../src/client/user/userClient';
const Device = () => {
const domNavigate = useDomNavigate();
const [storeId, setStoreId] = useState(() => JSON.parse(localStorage.getItem('storeId')));
const [currentDeposit, setCurrentDeposit] = useState<number>(0);
const depositBalanceInfo = userClient.getDepositBalanceByStoreId(storeId, isNil(storeId)).data;
const [time, setTime] = useState(1500);
const logout = () => {
localStorage.removeItem('accessToken');
domNavigate(`/home/home`);
};
//////////////////////////
// below is the code that I wrote to make it work..
const myFunc = () => {
// remove accessToken for logout
localStorage.removeItem('accessToken');
// move to home page
domNavigate(`/home/home`);
}
// begin setTimeOut automatically when the users come over to this page from another one.
useEffect(() => {
const timeoutBegins = window.setTimeout(myFunc, 3000);
return () => clearTimeout(timeoutBegins);
}, [])
// when the users click anything on the page, it clears current setTimeOut function and restart the setTimeOut function again.
const clickDisplay = () => {
clearTimeout(timeoutBegins);
timeOutBegins();
}
/////////////////////////////////////////
useEffect(() => {
if (storeId && depositBalanceInfo) {
setCurrentDeposit(depositBalanceInfo?.depositBalance);
}
}, [storeId, depositBalanceInfo?.depositBalance]);
return (
<ScreenContainer>
<Wrapper>
<div>Choose Payment Option</div>
<button onClick={() => window.history.back()}>go back</button>
<div>Your Balance: {currentDeposit.toLocaleString()}dollar</div>
<br />
<button onClick={() => domNavigate(`/charge/step2-select-price/?chargeMethod=card`)}>Credit Card Payment</button>
<br />
<button onClick={() => domNavigate(`/charge/step2-select-price/?chargeMethod=cash`)}>Cash Payment</button>
<br />
<button onClick={() => domNavigate(`/home/checkUserByPin`)}>Reset Password</button>
<br />
<button onClick={logout}>Logout</button>
</Wrapper>
</ScreenContainer>
);
};
const Wrapper = styled.div`
border: 1px solid red;
`;
export default Device;

You could do something like this. So, you run the timer and every time you clicking somewhere in the document it will cancel the current timer and run the new. So your myFunc function will run only if user doesn't click on the page after N seconds.
Keep in mind, you want to do it in 30 sec you will need to put 30000 in setTimeout
const timerId = useRef(null)
const myFunc = () => {
clearTimeout(timerId.current)
timerId.current = null
console.log('DO SOMETHING')
}
const onDocumentClick = () => {
if (timerId.current) {
clearTimeout(timerId.current)
timerId.current = window.setTimeout(myFunc, 3000)
}
}
useEffect(() => {
timerId.current = window.setTimeout(myFunc, 3000)
document.addEventListener('click', onDocumentClick)
return () => {
clearTimeout(timerId.current)
document.removeEventListener('click', onDocumentClick)
}
}, [])

There's a couple of issues that jump out from your snippet:
timeoutBegins is scoped to your useEffect callback so isn't available to clickDisplay.
clickDisplay is not attached to any event listeners
timeoutBegins is not callable, it is the timer ID
Note: it's a good idea to create a minimal reproducable example as this will help both you and reviewers eliminate the problem.
Solution:
let timerId;
function Device() {
const logOutUser = () => {
// log out code...
};
const startTimer = () => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(logOutUser, 3000);
}
const stopTimer = () => clearTimeout(timerId);
useEffect(() => {
// start timer when component is mounted
startTimer();
// stop timer when component is unmounted
return stopTimer;
}, []);
return <div onClick={startTimer}></div>;
}

Related

Create the setTimeout in the parent and closes it in the children

I have a specific problem that is keeping me awake this whole week.
I have a parent component which has a pop-up children component. When I open the page the pop-up shows off and after 5 seconds it disappears with a setTimeout.
This pop-up has an input element in it.
I want the pop-up to disappear after 5 seconds or if I click to digit something in the input. I tried to create a timerRef to the setTimeout and closes it in the children but it didn't work.
Can you help me, please? Thanks in advance.
ParentComponent.tsx
const ParentComponent = () => {
const [isVisible, setIsVisible] = useState(true)
timerRef = useRef<ReturnType<typeof setTimeout>>()
timerRef.current = setTimeout(() => {
setIsVisible(false)
}, 5000)
useEffect(() => {
return () => clearTimeout()
})
return (
<div>
<ChildrenComponent isVisible={isVisible} inputRef={timerRef} />
</div>
)
}
ChildrenComponent.tsx
const ChildrenComponent = ({ isVisible, inputRef}) => {
return (
<div className=`${isVisible ? 'display-block' : 'display-none'}`>
<form>
<input onClick={() => clearTimeout(inputRef.current as NodeJS.Timeout)} />
</form>
</div>
)
}
You're setting a new timer every time the the component re-renders, aka when the state changes which happens in the timeout itself.
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
Instead you can put the initialization in a useEffect.
useEffect(() => {
if (timerRef.current) return;
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
return () => clearTimeout(timerRef.current);
}, []);
You should also remove the "loose" useEffect that runs on every render, this one
useEffect(() => {
return () => clearTimeout();
});

how to create a timer inside hooks on Reactjs?

I am a starter at React! Started last week ;)
My first project is to create a timer which has a reset function and a second count function.
The reset function is working great, however the timer does not. Which is the best way to do it? It should increase +1s on variable 'second' according to the setTimeout() function.
Is it possible to create a loop on Hooks? I tried to do with the code below, but the page goes down, I think it is because the infinite loop that the code creates;
const [hour, setHour] = useState(4)
const [minute, setMinute] = useState(8)
const [second, setSecond] = useState(12)
// methods
const setTime = (value: string) => {
if (value === 'reset'){
setHour(0);
setMinute(0);
setSecond(0);
}
}
const startTime = () => {
while (second < 60){
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}
};
<div className="d-flex justify-content-center">
<MainButton
variantButton="outline-danger"
textButton="RESET"
functionButton={() => setTime('reset')}
/>
<MainButton
variantButton="outline-success"
textButton="START"
functionButton={() => startTime()}
/>
</div>
Welcome to React! You're very close. setTimeout and setInterval are very similar and for this you can simply use setInterval. No need for a while() loop! Check out this working Sandbox where I created a simple React Hook that you can use in your App.js
https://codesandbox.io/s/recursing-hooks-jc6w3v
The reason your code got caught in an infinite loop is because startTime() function has stale props. Specifically, the second variable is always 0 in this case, because when you defined startTime() on component mount, second was 0. The function doesn't track it's incrementing.
To resolve this issue, instead of:
setSecond(second + 1);
Try using:
setSecond((s) => s += 1);
EDIT* There are many good articles on React Stale Props. Here's one that's helpful: https://css-tricks.com/dealing-with-stale-props-and-states-in-reacts-functional-components/
EDIT** Additional inline examples of the exact issue:
Two changes I would make:
Use setInterval instead of setTimeout in a while() loop.
Create a useTimer hook which handles your timer logic.
App.js
import "./styles.css";
import useTimer from "./useTimer";
export default function App() {
const [setTime, startTime, stopTime, hour, minute, second] = useTimer();
return (
<div>
<div className="d-flex justify-content-center">
<button onClick={() => setTime("reset")}>RESET</button>
<button onClick={startTime}>START</button>
<button onClick={stopTime}>STOP</button>
</div>
<br />
<div>
Hour: {hour} <br />
Minute: {minute} <br />
Second: {second} <br />
</div>
</div>
);
}
useTimer.js
import { useState } from "react";
const useTimer = () => {
const [hour, setHour] = useState(4);
const [minute, setMinute] = useState(8);
const [second, setSecond] = useState(12);
const [timer, setTimer] = useState();
// methods
const clearTimer = () => clearInterval(timer);
const setTime = (value) => {
if (value === "reset") {
setHour(0);
setMinute(0);
setSecond(0);
}
};
const startTime = () => {
if (timer) clearTimer();
const newInterval = setInterval(() => {
setSecond((s) => (s += 1));
}, 1000);
setTimer(newInterval);
};
const stopTime = () => clearTimer();
return [setTime, startTime, stopTime, hour, minute, second];
};
export default useTimer;

How to correctly use Hooks in React?

I am new to React, and I have to build a timeout mechanism for a page. I used react-idle-timer, with some help found on the Internet. However, when I try to access the page, I get a Minified React error #321, in which it tells me that I used hooks incorrectly.
Can you please take a look on the following code and point me in the right direction? Thanks
import React from "react"
import NavBar from "./Navbar"
import "../styles/Upload.css"
import LinearProgressWithLabel from "./LinearProgressWithLabel"
import axios from "axios"
import Logout from "./Logout"
import { useIdleTimer } from 'react-idle-timer'
import { format } from 'date-fns'
export default function Upload() {
const [selectedFile, setSelectedFile] = React.useState();
const [progress, setProgress] = React.useState(0);
const timeout = 3000;
const [remaining, setRemaining] = React.useState(timeout);
const [elapsed, setElapsed] = React.useState(0);
const [lastActive, setLastActive] = React.useState(+new Date());
const [isIdle, setIsIdle] = React.useState(false);
const handleOnActive = () => setIsIdle(false);
const handleOnIdle = () => setIsIdle(true);
const {
reset,
pause,
resume,
getRemainingTime,
getLastActiveTime,
getElapsedTime
} = useIdleTimer({
timeout,
onActive: handleOnActive,
onIdle: handleOnIdle
});
const handleReset = () => reset();
const handlePause = () => pause();
const handleResume = () => resume();
React.useEffect(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
setInterval(() => {
setRemaining(getRemainingTime())
setLastActive(getLastActiveTime())
setElapsed(getElapsedTime())
}, 1000)
}, []);
function changeHandler(event) {
setSelectedFile(event.target.files[0])
};
function handleSubmission() {
if (selectedFile) {
var reader = new FileReader()
reader.readAsArrayBuffer(selectedFile);
reader.onload = () => {
sendFileData(selectedFile.name, new Uint8Array(reader.result), 4096)
};
}
};
function sendFileData(name, data, chunkSize) {
function sendChunk(offset) {
var chunk = data.subarray(offset, offset + chunkSize) || ''
var opts = { method: 'POST', body: chunk }
var url = '/api/uploaddb?offset=' + offset + '&name=' + encodeURIComponent(name)
setProgress(offset / data.length * 100)
fetch(url, opts).then(() => {
if (chunk.length > 0) {
sendChunk(offset + chunk.length)
}
else {
axios.post('/api/uploaddb/done', { name })
.then(setProgress(100))
.catch(e => console.log(e));
}
})
}
sendChunk(0);
};
return (
<div>
<NavBar />
<div>
<div>
<h1>Timeout: {timeout}ms</h1>
<h1>Time Remaining: {remaining}</h1>
<h1>Time Elapsed: {elapsed}</h1>
<h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
<h1>Idle: {isIdle.toString()}</h1>
</div>
<div>
<button onClick={handleReset}>RESET</button>
<button onClick={handlePause}>PAUSE</button>
<button onClick={handleResume}>RESUME</button>
</div>
</div>
<h1>Upload</h1>
<input type="file" name="file" onChange={changeHandler} />
{!selectedFile ? <p className="upload--progressBar">Select a file</p> : <LinearProgressWithLabel className="upload--progressBar" variant="determinate" value={progress} />}
<br />
<div>
<button disabled={!selectedFile} onClick={handleSubmission}>Submit</button>
</div>
</div>
)
}
Well, in this case, you should avoid setting states inside the useEffect function, because this causes an infinite loop. Everytime you set a state value, your component is meant to render again, so if you put states setters inside a useEffect function it will cause an infinite loop, because useEffect function executes once before rendering component.
As an alternative you can set your states values outside your useEffect and then put your states inside the useEffect array param. The states inside this array will be "listened" by useEffect, when these states change, useEffect triggers.
Something like this:
React.useEffect(() => {
}, [state1, state2, state3]);
state anti-pattern
You are using a state anti-pattern. Read about Single Source Of Truth in the React Docs.
react-idle-timer provides getRemainingTime, getLastActiveTime and getElapsedTime
They should not be copied to the state of your component
They are not functions
getRemainingTime(), getLastActiveTime(), or getElapsedTime() are incorrect
To fix each:
getRemainingTime should not be stored in state of its own
Remove const [remaining, setRemaining] = useState(timeout)
Remove setRemaining(getRemainingTime) both places in useEffect
Change <h1>Time Remaining: {remaining}</h1>
To <h1>Time Remaining: {getRemainingTime}</h1>
The same is true for lastActive.
getLastActive should be be stored in state of its own
Remove const [lastActive, setLastActive] = React.useState(+new Date())
Remove setLastActive(getLastActiveTime()) both places in useEffect
Change <h1>Last Active: {format(lastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
To <h1>Last Active: {format(getLastActive, 'MM-dd-yyyy HH:MM:ss.SSS')}</h1>
And the same is true for elapsed.
getElapsedTime should be be stored in state of its own
Remove const [elapsed, setElapsed] = React.useState(+new Date())
Remove setElapsed(getElapsedTime()) both places in useEffect
Change <h1>Time Elapsed: {elapsed}</h1>
To <h1>Time Elapsed: {getElapsedTime}</h1>
remove useEffect
Now your useEffect is empty and it can be removed entirely.
unnecessary function wrappers
useIdleTimer provides reset, pause, and resume. You do not need to redefine what is already defined. This is similar to the anti-pattern above.
Remove const handleReset = () => reset()
Change <button onClick={handleReset}>RESET</button>
To <button onClick={reset}>RESET</button>
Remove const handlePause = () => pause()
Change <button onClick={handlePause}>PAUSE</button>
To <button onClick={pause}>PAUSE</button>
Remove const handleResume = () => resume()
Change <button onClick={handleResume}>RESUME</button>
To <button onClick={resume}>RESUME</button>
avoid local state
timeout should be declared as a prop of the Upload component
Remove const timeout = 3000
Change function Upload() ...
To function Upload({ timeout = 3000 }) ...
To change timeout, you can pass a prop to the component
<Upload timeout={5000} />
<Upload timeout={10000} />
use the provided example
Read Hook Usage in the react-idle-timer docs. Start there and work your way up.
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import App from './App'
export default function (props) {
const handleOnIdle = event => {
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
}
const handleOnActive = event => {
console.log('user is active', event)
console.log('time remaining', getRemainingTime())
}
const handleOnAction = event => {
console.log('user did something', event)
}
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * 15,
onIdle: handleOnIdle,
onActive: handleOnActive,
onAction: handleOnAction,
debounce: 500
})
return (
<div>
{/* your app here */}
</div>
)
}

Retain callback between rerenders

I'm building an app with guarded actions and would like to achieve the following flow:
anonymous user tries to run guarded action (e.g. play video - essentially modify React state)
login modal pops up without any redirects
user fills in the credentials and hits login button
any kind of loader shows up (and replaces the previous app tree) and stays on the screen as long as user is being logged in
once logged in, loader disappears and the authenticated app renders
video starts playing, because the app knows the action user wanted to take
I haven't found a way to do that, because replacing the old app tree with loader and then putting it back on the screen makes the action run on an already unmnounted component.
I'm using a <UserProvider> component that wraps the whole tree to provide it with authenticated state.
Codesandbox to illustrate the issue:
https://codesandbox.io/s/react-context-callback-l9xe9?file=/src/App.js
const MyContext = React.createContext();
const MyProvider = ({ children }) => {
const [auth, setAuth] = React.useState(false);
const [reset, setReset] = React.useState(false);
const [callback, setCallback] = React.useState(null);
const guard = (cb) => {
auth ? cb() : setCallback(() => cb);
};
React.useEffect(() => {
if (auth) {
setReset(true);
setTimeout(() => {
setReset(false);
}, 1000);
}
}, [auth]);
if (reset) {
return <p style={{ color: "red" }}>I pretend I'm logging you in</p>;
}
return (
<MyContext.Provider
value={{
setAuth,
auth,
callback,
setCallback,
guard
}}
>
{children}
</MyContext.Provider>
);
};
const Child = () => {
const [count, setCount] = React.useState(0);
const mounted = React.useRef(false);
React.useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const myCallback = () => {
setCount((cnt) => cnt + 1);
};
const { auth, setAuth, guard, callback, setCallback } = React.useContext(
MyContext
);
return (
<div>
logged in: {`${auth}`}
<br />
count: {count}
<br />
{callback && (
<button
onClick={() => {
setAuth(true);
callback();
setCallback(null);
}}
>
pretend login
</button>
)}
<button onClick={() => guard(myCallback)}>
increment (when authenticated)
</button>
</div>
);
};
Is that even possible?
That's a use case for lifting the state up, you can't preserve the state after the component unmounts (Of course you can try reading from local storage but it's essentially the same logic as lifting the state).
Therefore lift the count state (to App or to the provider itself):
export default function App() {
const state = React.useState(0);
...

Why does my code in React JS goes to endless loop satisfying both conditions?

import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import firebase from '../../config/firebaseConfig'
import SingleEventSummary from './SingleEventSummary'
import { getEvent } from "./../../store/actions/eventActions";
import "./SingleEvent.css";
const SingleEvent = props => {
const id = props.match.params.id;
const [eventItem, seteventItem] = useState([]);
const [isFavourite, setIsFavourite] = useState("no");
//getting specific event
useEffect(() => {
const db = firebase.firestore().collection('newEvents').doc(id)
db.onSnapshot(snapshot => {
seteventItem(snapshot.data())
})
}, [id])
//checking if there is favourite
useEffect(() => {
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.get().then(snapshot => {
const data = snapshot.data()
const faves = data && snapshot.data().favorites || []
faves.includes(id) ? setIsFavourite("yes") : setIsFavourite("no")
},(error) => console.error(error))
},[isFavourite])
//setting as favourites
const favouriteClick = (uid, eid) => {
debugger;
let initFav = firebase.firestore().collection('users').doc(uid);
initFav.get().then(snapshot => {
const arrayUnion = firebase.firestore.FieldValue.arrayUnion(eid);
initFav.update({
favorites: arrayUnion,
});
setIsFavourite("yes")
},(error) => console.error(error))
}
//remove favourites
const removeFavourite = () => {
debugger;
const initFavo = firebase.firestore().collection('users').doc(props.auth.uid);
initFavo.get().then(snapshot => {
if (snapshot.data().favorites) {
if (snapshot.data().favorites.includes(id)) {
let data = snapshot.data().favorites.filter(el => el != id )
initFavo.update({
favorites: data,
});
setIsFavourite("no")
}
}
},(error) => console.error(error))
return () => initFavo()
}
console.log("wtf is this shit", isFavourite)
if (isFavourite == "no") {
return (
<main className="single-event_main">
<a className="waves-effect waves-light btn" onClick={favouriteClick(props.auth.uid, id)}>Add As Favourites!!</a>
</main>
);
}
else {
return (
<main className="single-event_main">
<div className="row">
<div className="col s6">
<a className="waves-effect waves-light btn" disabled>Favourite Event!!</a>
</div>
<div className="col s6">
<a className="waves-effect waves-light btn" onClick={removeFavourite}>Remove From Favourites!!</a>
</div>
</div>
</main>
);
}
};
export default SingleEvent;
I am trying to set the value in hook, comparing if the event id exists in the user's database(if he/she has set that event as a favourite).
....
const [isFavourite, setIsFavourite] = useState("no");
//checking if there is favourite
useEffect(() => {
debugger;
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.onSnapshot(snapshot => {
debugger;
if(snapshot.data()) {
if (snapshot.data().favorites) {
if (snapshot.data().favorites.includes(id)) {
setIsFavourite("yes")
}
else if(!snapshot.data().favorites.includes(id)){
setIsFavourite("no")
}
}
}
}, (error) => console.error(error));
return () => db()
},[])
....
The issue is, react goes inside both conditions endlessly setting the hook value both yes and no. Been stuck on this hours.
Any kind of help will be much appreciated!!!
jus offering a little refactor -> this is just a bit easier to read
useEffect(() => {
const db = firebase.firestore().collection('users').doc(props.auth.uid)
db.onSnapshot(snapshot => {
const data = snapshot.data()
const faves = data && snapshot.data().favorites || []
faves.includes(id) ? setIsFavourite("yes") : setIsFavourite("no")
},(error) => console.error(error))
return () => db()
},[])
I can't see why your code would be looping perhaps we need more code as the above commenter mentioned.
Ok now that you have shown us more code. I can say with a large degree of confidence it is because you are calling favouriteClick in the onClick of the "Add As Favourites" button.
Which is causing a weird loop.
Change
onClick={favouriteClick(props.auth.uid, id)
to
onClick={() => favouriteClick(props.auth.uid, id)
You are welcome!
you should have a stop condition for this hook, useEffect hook is triggered every time you render something, so you ending up changing props and rendering and then trigger useEffect which change props and trigger render lifecycle Hook.
You should have something like that
useEffect(() => {
// call database
},[setFavorite]) // here goes stop condition for useEffect
If setFavorite is still false it won't trigger trigger again, or if setFavorite is false and request from db is setting it to true then next time if you it's trigger useEffect again and setFavorite is still true then useEffect won't execute.
For more details read officials documentation https://reactjs.org/docs/hooks-effect.html
If you are updating firebase in setIsFavourite() then you are creating a change that will be picked up by the .onSnapshot listener. This will force an endless loop of : condition triggers change > listens for condition > condition triggers change > ad Infinitum.
You either need to switch from .onSnapshot to a one-off .get listener or you need to add a condition to prevent changes from propagating in this case. This custom condition will be specific to your implementation, not really something we can help with unless you show use more code and help us understand what you are trying to achieve (that should be a separate question though). So I suspect the former in this case.

Resources