Search code examples
javascriptreactjsreact-hooks

Issue with idle time using ReactJS and JavaScript


I need to detect the idle time and automatically show a dialog that will say "You've been inactive for a while. To keep your session active, please click the button below"

When he clicks that button, it would reset the time so it wouldn't call the logout. Right now its still calling it.

However it doesn't work well. Is there a better approach or recommended way to handling idle time in my app or is this ok?

Here is the codesandbox CLICK HERE

import { Button, Dialog } from "@mui/material";
import DialogHeader from "./components/DialogHeader";
import DialogBody from "./components/DialogBody";
import DialogFooter from "./components/DialogFooter";
import { useIdleSession } from "./hooks/useIdleSession";

function App() {
  const handleLogout = () => {
    console.log("logout");
  };

  const IDLE_CONFIG = {
    threshold: 5000, // idle time threshold in milliseconds
    timeout: 8000, // timeout duration in milliseconds
    action: handleLogout // action to be taken when timeout is reached
  };

  const { open, idleTime, handleUserActivity } = useIdleSession(IDLE_CONFIG);

  return (
    <>
      <h1>HELLO WORLD</h1>
      <Dialog open={open}>
        <DialogHeader>
          {" "}
          You&#39;ve been idle for {idleTime / 1000} seconds
        </DialogHeader>

        <DialogBody>
          You&#39;ve been inactive for a while. To keep your session active,
          please click the button below
        </DialogBody>
        <DialogFooter>
          <Button
            color="primary"
            variant="contained"
            onClick={handleUserActivity}
          >
            Keep Session Active
          </Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

export default App;

useIdleSession

import { useEffect, useState, useCallback } from "react";

export const useIdleSession = (config) => {
  const [lastActive, setLastActive] = useState(new Date());
  const [open, setOpen] = useState(false);
  const [idleTime, setIdleTime] = useState(0);
  const [logoutIntervalId, setLogoutIntervalId] = useState(null);
  const isLoggedIn = true;

  //function to handle user activity
  const handleUserActivity = useCallback(() => {
    setLastActive(new Date());
    setOpen(false);
    clearTimeout(logoutIntervalId);
  }, [logoutIntervalId]);

  // useEffect to check for user activity every minute
  useEffect(() => {
    if (isLoggedIn) {
      // setInterval to check for user activity
      const intervalId = setInterval(() => {
        // get current time
        const currentTime = new Date();
        // calculate idle time
        const idle = currentTime - lastActive;
        // check if user has been idle for more than the threshold time
        if (idle > config.threshold) {
          setIdleTime(idle);
          setOpen(true);
          // set a timeout to run the action if they do not click the "Keep Session Active" button
          const newLogoutIntervalId = setTimeout(config.action, config.timeout);
          setLogoutIntervalId(newLogoutIntervalId);
        }
      }, config.threshold);
      // cleanup function to clear interval when component unmounts
      // document.addEventListener('mousemove', handleUserActivity);
      // document.addEventListener('keypress', handleUserActivity);
      return () => {
        clearInterval(intervalId);
        // document.removeEventListener('mousemove', handleUserActivity);
        // document.removeEventListener('keypress', handleUserActivity);
      };
    }
  }, [isLoggedIn, lastActive, handleUserActivity, config]);

  return { open, idleTime };
};

Solution

  • The useIdleSession doesn't return any handleUserActivity property, e.g. return { open, idleTime, handleUserActivity };. handleUserActivity is undefined in calling components.

    Return the declared handleUserActivity callback function so it can be destructured from the hook. I recommend also storing the timer/interval id in React refs so they can be used to also correctly clear any running timers on the event of component unmounting.

    export const useIdleSession = (config) => {
      const [lastActive, setLastActive] = useState(new Date());
      const [open, setOpen] = useState(false);
      const [idleTime, setIdleTime] = useState(0);
    
      const intervalId = useRef();
      const logoutIntervalId = useRef();
    
      const isLoggedIn = true;
    
      // function to handle user activity
      const handleUserActivity = useCallback(() => {
        setLastActive(new Date());
        setOpen(false);
        clearTimeout(logoutIntervalId.current);
      }, []);
    
      // Effect to return cleanup function to clear any running timers upon
      // component unmount.
      useEffect(() => {
        return () => {
          clearInterval(intervalId.current);
          clearTimeout(logoutIntervalId.current);
        };
      }, []);
    
      // Effect to check for user activity periodically
      useEffect(() => {
        if (isLoggedIn) {
          // setInterval to check for user activity
          intervalId.current = setInterval(() => {
            // get current time
            const currentTime = new Date();
            // calculate idle time
            const idle = currentTime - lastActive;
            // check if user has been idle for more than the threshold time
            if (idle > config.threshold) {
              setIdleTime(idle);
              setOpen(true);
              // set a timeout to run the action if they do not click the "Keep Session Active" button
              logoutIntervalId.current = setTimeout(config.action, config.timeout);
            }
          }, config.threshold);
          return () => {
            clearInterval(intervalId.current);
          };
        }
      }, [isLoggedIn, lastActive, handleUserActivity, config]);
    
      return { open, idleTime, handleUserActivity };
    };
    

    Edit issue-with-idle-time-using-reactjs-and-javascript

    The useIdleSession hook logic can be simplified a bit. There's no need really to store a datestamp as the basic gist is to just instantiate a timeout that expires after the specified idle timeout, and upon any interactive event by a user to clear the previous timer and reinstantiate. It only needs to return a function to the component to manually reset, if/when necessary.

    Example:

    export const useIdleSession = ({ timeout, onExpire }) => {
      const timerId = useRef();
    
      //function to handle user activity
      const handleUserActivity = useCallback(() => {
        clearTimeout(timerId.current);
        timerId.current = setTimeout(onExpire, timeout);
      }, [onExpire, timeout]);
    
      useEffect(() => {
        if (isLoggedIn) {
          // Listen for user activity
          document.addEventListener("mousemove", handleUserActivity, {
            passive: true
          });
          document.addEventListener("mousedown", handleUserActivity);
          document.addEventListener("keydown", handleUserActivity);
          document.addEventListener("touchstart", handleUserActivity);
    
          handleUserActivity();
    
          return () => {
            document.removeEventListener("mousemove", handleUserActivity, {
              passive: true
            });
            document.removeEventListener("mousedown", handleUserActivity);
            document.removeEventListener("keydown", handleUserActivity);
            document.removeEventListener("touchstart", handleUserActivity);
          };
        }
      }, [ handleUserActivity]);
    
      useEffect(() => {
        return () => {
          clearTimeout(timerId.current);
        };
      }, []);
    
      return { reset: handleUserActivity };
    };
    

    Edit issue-with-idle-time-using-reactjs-and-javascript (forked)