React Hooks Idle Timeout for Functional Component - javascript

I have an application that requires an idle timeout that first warns the user that they will be logged out in one minute, then logs the user out after the one minute has expired.
I have had success using a class component as demonstrated in the following post:
Session timeout warning modal using react
I am moving the code for my application over to React Hooks but I'm having a hard time moving this code over. I have tried the following:
const [signoutTime, setSignoutTime] = useState(0);
let warnTimeout;
let logoutTimeout;
const setTimeout = () => {
warnTimeout = setTimeout(warn, warningTime);
logoutTimeout = setTimeout(logout, signoutTime);
};
const clearTimeout = () => {
if (warnTimeout) clearTimeout(warnTimeout);
if (logoutTimeout) clearTimeout(logoutTimeout);
};
useEffect(() => {
setWarningTime(10000);
setSignoutTime(15000);
const events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
const resetTimeout = () => {
clearTimeout();
setTimeout();
};
for (var i in events) {
window.addEventListener(events[i], resetTimeout);
}
setTimeout();
});
const warn = () => {
console.log('Warning');
};
const destroy = () => {
console.log('Session destroyed');
};
In the end, I would like for a modal to appear to warn the user of impending logout. If the user moves the mouse, clicks, etc (see events) then the timer resets. If the user clicks on the button in the modal the timer is reset.
Thanks so much for the help!

Try this
mport React, { useEffect, useState } from 'react';
const LogoutPopup = () => {
const [signoutTime, setSignoutTime] = useState(10000);
const [warningTime, setWarningTime] = useState(15000);
let warnTimeout;
let logoutTimeout;
const warn = () => {
console.log('Warning');
};
const logout = () => {
console.log('You have been loged out');
}
const destroy = () => {
console.log('Session destroyed');
}
const setTimeouts = () => {
warnTimeout = setTimeout(warn, warningTime);
logoutTimeout = setTimeout(logout, signoutTime);
};
const clearTimeouts = () => {
if (warnTimeout) clearTimeout(warnTimeout);
if (logoutTimeout) clearTimeout(logoutTimeout);
};
useEffect(() => {
const events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
const resetTimeout = () => {
clearTimeouts();
setTimeouts();
};
for (let i in events) {
window.addEventListener(events[i], resetTimeout);
}
setTimeouts();
return () => {
for(let i in events){
window.removeEventListener(events[i], resetTimeout);
clearTimeouts();
}
}
},[]);
return <div></div>
}
export default LogoutPopup;

Based on the first answer, I have another hooks solution when you also need the time until the user gets logged out.
Codesandbox Demo
useAutoLogout.js
import React, { useEffect, useState } from "react";
const useLogout = (startTime) => {
const [timer, setTimer] = useState(startTime);
useEffect(() => {
const myInterval = setInterval(() => {
if (timer > 0) {
setTimer(timer - 1);
}
}, 1000);
const resetTimeout = () => {
setTimer(startTime);
};
const events = [
"load",
"mousemove",
"mousedown",
"click",
"scroll",
"keypress"
];
for (let i in events) {
window.addEventListener(events[i], resetTimeout);
}
return () => {
clearInterval(myInterval);
for (let i in events) {
window.removeEventListener(events[i], resetTimeout);
}
};
});
return timer;
};
export default useLogout;
App.js
import useAutoLogout from "./useAutoLogout";
function App() {
const timer = useAutoLogout(10);
if (timer == 0) {
return <div>Logged Out</div>;
}
if (timer < 8) {
return <div>In {timer} seconds you will be automatically logged out</div>;
}
return <div>Signed in</div>;
}
export default App;

I rewrote your answer a bit to get rid of liniting errors, I thought it would also be handy to extract it into a hook so then you just need to pass in the parameters you want, that eliminates the need to have the states for the timers.
The useCallback I suppose is for efficiency to stop it working itself out on each render,
I've moved some of the other stuff inside the UseEffect to get rid of those linting errors!
import React, { useCallback } from 'react'
function useMonitor({ timeout, warningTimeout, onWarn, onTimeOut }) {
const warn = useCallback(()=> {
onWarn && onWarn()
},[onWarn])
const logout = useCallback(()=> {
onTimeOut && onTimeOut()
},[onTimeOut])
React.useEffect(() => {
let warnTimeout;
let logoutTimeout;
const setTimeouts = () => {
warnTimeout = setTimeout(warn, warningTimeout);
logoutTimeout = setTimeout(logout, timeout);
};
const clearTimeouts = () => {
if (warnTimeout) clearTimeout(warnTimeout);
if (logoutTimeout) clearTimeout(logoutTimeout);
};
const events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
const resetTimeout = () => {
clearTimeouts();
setTimeouts();
};
for (let i in events) {
window.addEventListener(events[i], resetTimeout);
}
setTimeouts();
return () => {
for (let i in events) {
window.removeEventListener(events[i], resetTimeout);
clearTimeouts();
}
}
}, [logout, timeout, warn, warningTimeout]);
}
export default useMonitor

Related

How to stop setInterval when tab is not active while using useEffect?

I'm using setInterval in useEffect. When use not actively using tab, I't requesting like forever. It causing some memory issue. I have to, fetch data in every 3000ms, also stop it when user is not using this tab actively. How can I do such a thing?
I tried to use document.visibiltyState and I couldn't worked it.
My code:
useEffect(() => {
try {
const interval = setInterval(() => {
getTransactionGroupStats()
getTransactionGroups()
}, 3000)
getBinanceBalanceStats()
return () => {
clearInterval(interval)
}
} catch (error) {
console.error(error)
}
}, [])
Another alternative and a little more scalable could be that you create a custom hook to see if the user is active or not and every time it changes you execute the useEffect
useActive.ts
export const useActive = (time: number) => {
const [active, setActive] = useState(false)
const timer: any = useRef()
const events = ['keypress', 'mousemove', 'touchmove', 'click', 'scroll']
useEffect(() => {
const handleEvent = () => {
setActive(true)
if (timer.current) {
window.clearTimeout(timer.current)
}
timer.current = window.setTimeout(() => {
setActive(false)
}, time)
}
events.forEach((event: string) => document.addEventListener(event, handleEvent))
return () => {
events.forEach((event: string) => document.removeEventListener(event, handleEvent))
}
}, [time])
return active
}
YourApp.tsx
const active = useActive(3000)
useEffect(() => {
if(active){
try {
const interval = setInterval(() => {
getTransactionGroupStats()
getTransactionGroups()
}, 3000)
getBinanceBalanceStats()
return () => {
clearInterval(interval)
}
} catch (error) {
console.error(error)
}
}
}, [active])
I solved with this approach: https://blog.sethcorker.com/harnessing-the-page-visibility-api-with-react/
export function usePageVisibility () {
const [isVisible, setIsVisible] = useState(!document.hidden)
const onVisibilityChange = () => setIsVisible(!document.hidden)
React.useEffect(() => {
document.addEventListener('visibilitychange', onVisibilityChange, false)
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange)
}
})
return isVisible
}

why my code calling API for multiple time instedof just once after delaying of 500 ms using debounce

I'm trying to call API using debounce but in this case, API calling for every character,
for example, I type hello in search then it calls for he, hel, hell, and hello but I want only for final word hello
useEffect(() => {
updateDebounceWord(word);
}, [word]);
const updateDebounceWord = debounce(() => {
{
word.length > 1 && dictionaryApi();
}
});
function debounce(cb, delay = 500) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
cb(...args);
}, delay);
};
}
const dictionaryApi = async () => {
// inital state []
console.log("hited")
try {
const data = await axios.get(
`https://api.dictionaryapi.dev/api/v2/entries/${category}/${word}`
);
console.log("Fetched",word);
setMeanings(data.data);
} catch (e) {
console.log("error||", e);
}
};
In addition to Dilshans explanation, I wan't to suggest making a hook out of your debounce function, so you can easily reuse it:
const useDebounce = (cb, delay = 500) => {
const timer = useRef();
// this cleans up any remaining timeout when the hooks lifecycle ends
useEffect(() => () => clearTimeout(timer.current), [cb, delay]);
return useCallback(
(...args) => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
cb(...args);
}, delay);
},
[cb, delay]
);
};
use it like this in your components:
const updateDebounceWord = useDebounce((word) => {
console.log("api call here", word);
});
useEffect(() => {
updateDebounceWord(word);
}, [word, updateDebounceWord]);
You are using the debounce on render phase of the component. so each time when the component rebuild a new tree due to the state update, the updateDebounceWord will redeclare. Both current and workInProgress node of the component will not share any data. If you want to share the data between current and workInProgress tree use useRef or else put in global scope
A quick fix is, put the timer variable in global scope.
// keep this on global scope
let timer = null;
function debounce(cb, delay = 500) {
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
cb(...args);
}, delay);
};
}
export default function App() {
const [word, setWord] = useState("");
const sendReq = debounce((keyword) => {
apiReq(keyword);
})
useEffect(() => {
if (word.length > 0) {
sendReq(word);
}
}, [word, sendReq])
const apiReq = (keyword) => {
console.log('reached', keyword);
}
return (
<div className="App">
<input value={word} onChange={(e) => setWord(e.target.value)} />
</div>
);
}
Also put all the dependencies in the useEffect dep array otherwise it may not work as expected.
useEffect(() => {
updateDebounceWord(word);
}, [word, updateDebounceWord]);

useEffect only on pageload AND page refresh

I have a functional component that should subscribe to events when the page loads and when the page refreshes and unsubscribe each time the component unmounts. I want to use useEffect for that, unfortunately not successfully. The first attempt removes eventlisteners and adds eventlisteners again each time a new event is passed:
import { useEffect } from 'react';
import { useSnackbar } from '../../context/SnackbarContext';
import { parseRigData } from '../data.utils';
import sse from '../../lib/sse';
export function useRig(config) {
const { openSnackbar } = useSnackbar();
useEffect(() => {
const createListeners = () => {
const newListeners = {};
config.forEach(instance => {
const { name, handleData, showMessage, type } = instance;
newListeners[name] = ({ data }) => {
// eslint-disable-next-line no-console
console.log('[RIG] received', JSON.parse(data, 0, 2));
const messageBody = parseRigData(data);
handleData(messageBody);
if (showMessage) {
const message = showMessage(messageBody);
if (message) openSnackbar(message, type);
}
};
});
return newListeners;
};
const initializeListeners = listeners => {
config.forEach(instance => {
const { events, name } = instance;
sse.listenForUserMessage(events, listeners[name]);
});
};
const removeListeners = listeners => {
config.forEach(instance => {
const { events, name } = instance;
sse.removeListener(events, listeners[name]);
});
};
const createConnection = async (events, listeners) => {
if (!events.length) return;
await sse.connect(events);
initializeListeners(listeners);
};
function getEvents() {
const result = [];
config.forEach(instance => {
const { events, config } = instance;
result.push({
eventTypes: events,
config
});
});
return result;
}
const events = getEvents();
const listeners = createListeners();
createConnection(events, listeners);
return () => {
removeListeners(listeners);
if (events.length) sse.disconnect(events);
};
}, [config, openSnackbar]);
}
I understand the behaviour - it happens each time the component updates. Thats why I tried to solve this problem by removing config and openSnackbar from the useEffect-array that defines what changes are being watched for the useEffect to take place:
export function useRig(config) {
const { openSnackbar } = useSnackbar();
useEffect(() => {
// ...same code
return () => {
removeListeners(listeners);
if (events.length) sse.disconnect(events);
};
}, []);
}
Unfortunately, this is not working when I reload the page with f5. In that case, the events are not passed ("canĀ“t perform a React state update on an unmounted component) and the listeners from the return cleanup are not removed. Otherwise, on a normal page load or leaving page it works great.
Is there any way how can I include page reload(but not componentUpdate) to perform useEffect?
as i can see there is a problem of calling the create listeners
you created the function inside the useEffect but not calling it ,
try to create it outside and call it on the useEffect like you did with removeListeners
useEffect(() => {
console.log("onMounte with useEffect");
const createListeners = () => {
console.log("createListeners called"); // this will not work ...
};
return () => {
console.log("unmount") // will be called when the component going to be unmounted
};
}, []);

How to run a function in React when the Escape button has been pressed?

The whole idea of the component is that the first time the Escape key is pressed, the value confirm in state changes to true to verify that the user really wants to execute the secPress() function and pressing the Escape key again within 5 seconds run the secPress() function.
Unfortunately, the secPress() function in the component never run.
How to get the function to run after the second key press?
import React, { useState, useEffect, useCallback } from "react";
export function useKeypress(key, action) {
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
window.addEventListener("keyup", onKeyup);
return () => window.removeEventListener("keyup", onKeyup);
}, []);
}
const App = () => {
const [confirm, setConfirm] = useState(false);
// this function never run
const secPress = () => {
console.log("Double escape pressed");
};
const _confirm = useCallback(() => {
setConfirm((prev) => !prev);
console.log(confirm); // <-- always show false
if (confirm) {
secPress();
}
}, [confirm]);
useKeypress("Escape", _confirm);
useEffect(() => {
const d = setTimeout(() => {
setConfirm(false);
}, 5000);
return () => {
clearTimeout(d);
};
}, [confirm]);
return <>{confirm ? "true" : "false"}</>;
};
export default App;
React version: 17.0.2
In your case, it's because you're passing an empty dependencies array to the useEffect inside the useKeypress function; this way, the event listener has always the same callback.
The easiest change would be to pass the dependencies, for example:
import React, { useState, useEffect, useCallback } from "react";
export function useKeypress(key, action, deps) {
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
window.addEventListener("keyup", onKeyup);
return () => window.removeEventListener("keyup", onKeyup);
}, deps);
}
const App = () => {
const [confirm, setConfirm] = useState(false);
// this function never run
const secPress = () => {
console.log("Double escape pressed");
};
const _confirm = useCallback(() => {
setConfirm(prev => !prev);
console.log(confirm); // <-- always show false
if (confirm) {
secPress();
}
}, [confirm]);
useKeypress("Escape", _confirm, [confirm]);
useEffect(() => {
const d = setTimeout(() => {
setConfirm(false);
}, 5000);
return () => {
clearTimeout(d);
};
}, [confirm]);
return <>{confirm ? "true" : "false"}</>;
};
export default App;
The previous answer is incorrect, you shouldn't have to add [confirm] to the event listener effect
working example:
https://codesandbox.io/s/00rxv
const [stage, setStage] = useState("initial");
const secPress = () => {
console.log("Double escape pressed");
};
const keydown = (e) => {
if (e.key === "Escape") {
setStage((current) => (current === "initial" ? "confirm" : "success"));
}
};
useEffect(() => {
window.addEventListener("keydown", keydown);
return () => window.removeEventListener("keydown", keydown);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (stage === "success") {
secPress();
}
if (stage !== "confirm") {
return;
}
const d = setTimeout(() => {
setStage("initial");
}, 2000);
return () => {
clearTimeout(d);
};
}, [stage]);

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