DEV Community

Nilesh Kumar
Nilesh Kumar

Posted on

How to Track User Inactivity on Your Website and Why It Matters

In today’s web applications, ensuring that users remain active while maintaining good user experience is essential. One common scenario is detecting user inactivity and displaying a reminder or warning to keep users engaged before logging them out due to inactivity.

In this blog, we'll break down the concept of user inactivity tracking with a React app and show you how to implement a "Are you active?" prompt, using a timeout system to track the user's activity on the page. Let’s explore the code behind it and how it works.

inactive modal if user is not active for a while

Problem Statement:

You might have noticed websites that log you out automatically after a certain period of inactivity. This happens because, in some cases, it’s important to maintain security (to avoid leaving sensitive information open if a user walks away from their device) or optimise server resources. So, how do we detect this inactivity in a React app?

Code Breakdown

Let’s go through the code step by step to understand how user inactivity is tracked, and how the "Are you active?" prompt is displayed.

1. React States & Context Setup

const [login, setLogin] = useState("");
const [isActive, setIsActive] = useState(true);
const [showPrompt, setShowPrompt] = useState(false);
Enter fullscreen mode Exit fullscreen mode
  • login: Holds the user's login token to determine if the user is logged in.
  • isActive: Tracks whether the user is active or not. Initially set to true.
  • showPrompt: Controls whether the "Are you active?" prompt is shown.

2. Timeout Logic

const timeoutId = useRef(null);
Enter fullscreen mode Exit fullscreen mode
  • timeoutId: A reference to store the timeout ID, which helps in clearing the timeout when needed (such as when the user interacts with the page).

3. Reset Timer on Activity

const resetTimer = () => {
  if (showPrompt) {
    return;
  }
  setIsActive(true);
  setShowPrompt(false);
  clearTimeout(timeoutId.current);
  if (login) {
    timeoutId.current = setTimeout(() => {
      setIsActive(false);
      setShowPrompt(true);
    }, 13 * 60 * 1000); // 13 minutes timeout
  }
};
Enter fullscreen mode Exit fullscreen mode
  • The resetTimer function resets the timer whenever the user interacts with the website (like moving the mouse, pressing keys, etc.).
  • If the user doesn’t interact for 13 minutes, it triggers the display of the "Are you active?" prompt.
  • The function also makes sure not to show the prompt repeatedly if the user is already interacting.

4. Activity Event Listener

const handleActivity = () => {
  if (!isActive) {
    resetTimer();
  }
};
Enter fullscreen mode Exit fullscreen mode

This function listens for events such as mouse movements, key presses, and clicks, indicating that the user is still active on the page.
It calls resetTimer() if the user is inactive and then interacts with the page again.

5. Auto Logout if No Action Taken

const logout = () => {
  setShowPrompt(false);
  cleanLocalStorage(); // Clears any saved session data
  navigate("/login");
  setLogin(""); // Resets the login state
}
Enter fullscreen mode Exit fullscreen mode

If the user doesn’t respond to the "Are you active?" prompt within a set time (2 minutes), the **logout()** function is called, logging the user out automatically.

6. Using useEffect to Handle Events

First useEffect Hook: Checking for a Login Token

useEffect(() => {
  const token = localStorage.getItem('token');
  if (token) {
    setLogin(token);
  }
}, [login]);
Enter fullscreen mode Exit fullscreen mode

This hook is used to check whether a user is logged in when the component mounts or when the login state changes. Here’s how it works:

  • useEffect runs on component mount or state change:

useEffect is a special React hook that allows you to run some code when the component first renders, or when a specific state or prop changes. In this case, the effect will run whenever the login state changes, which is passed as the second argument [login].

  • Check for a "token" in localStorage:

The code inside the useEffect looks for a token stored in the browser’s localStorage. localStorage is a simple key-value store in the browser that persists even if the page reloads or the user navigates to another page. The key here is "token", and getItem('token') retrieves the value associated with that key.

  • If a token is found, update the login state:

If a token is found in localStorage, the setLogin(token) function is called to update the login state with the token's value. This might indicate that the user has logged in and their session is still active (as the token is typically used for authentication).

  • Why [login] as the dependency array?:

The useEffect will run again whenever the login state changes. However, this approach might cause an issue: because the setLogin function changes the login state, it could cause the useEffect to loop continuously, running each time login changes. In most cases, you would want to set the token only once when the component mounts, so using useEffect with [login] may not be ideal. You might want to pass an empty array [] to run this effect only once when the component mounts, instead of each time login changes.

Second useEffect Hook: Handling User Activity

useEffect(() => {
  if (login) {
    window.addEventListener('mousemove', handleActivity);
    window.addEventListener('keydown', handleActivity);
    window.addEventListener('click', handleActivity);
    resetTimer();
    return () => {
      window.removeEventListener('mousemove', handleActivity);
      window.removeEventListener('keydown', handleActivity);
      window.removeEventListener('click', handleActivity);
      clearTimeout(timeoutId.current);
    };
  }
  setIsActive(false);
}, [login]);
Enter fullscreen mode Exit fullscreen mode

This useEffect handles user activity detection and manages event listeners that track whether the user is interacting with the page. Here's how it works:

- useEffect listens for changes in login state:

This useEffect hook depends on the login state (just like the previous one). It runs whenever the login state changes, which means it will run:
After the initial render (when the component mounts), and
Whenever the login state updates.

- If the user is logged in (login is truthy):

The first thing the hook does is check if login has a truthy value (i.e., the user is logged in).
If login exists (meaning the user is logged in), it sets up three event listeners on the window object:
mousemove: Detects if the user moves the mouse.
keydown: Detects if the user presses any keys on the keyboard.
click: Detects if the user clicks anywhere on the page.

These event listeners are used to track user activity. Every time one of these events occurs, it will trigger the handleActivity function, which will presumably reset the inactivity timer, ensuring the user is considered active and not logged out due to inactivity.

- Reset the inactivity timer:

resetTimer() is called after setting up the event listeners. This function likely resets a timeout timer that keeps track of how long the user has been inactive.

- Clean up event listeners on unmount or when login changes:

The return statement inside the useEffect defines a cleanup function that is executed when:
The component is about to unmount, or
The login state changes.

- Inside the cleanup function:
The window.removeEventListener methods are used to remove the event listeners that were previously set up. This is important to avoid memory leaks and prevent unnecessary listeners running after the user logs out or the component is removed.
clearTimeout(timeoutId.current) is used to clear the timeout that might still be running, so no unnecessary logout logic is triggered.

  • If login is falsy (user is not logged in):

If the login state is falsy (the user is not logged in), the function calls setIsActive(false) to set the user's activity state to false. This might indicate that the user is not active because they are logged out, and no activity is being tracked.

7. Showing the "Are You Active?" Prompt

<ConfirmPopup
  open={showPrompt}
  setOpen={setShowPrompt}
  setAccepted={resetTimer}
  message={"You will be logged out soon due to inactivity. Click 'Continue' to stay logged in."}
  handleNo={logout}
  yesBtn={"Continue"}
  hideNoBtn={true}
/>
Enter fullscreen mode Exit fullscreen mode
  • The ConfirmPopup component is a modal that displays the "Are you active?" message with the option to either stay logged in or be logged out.
  • setAccepted={resetTimer} means that clicking "Continue" resets the inactivity timer, keeping the user logged in.
  • handleNo={logout} means clicking "No" will log the user out after the specified time.

## You can customise your modal as you want.

Complete working code

import { useEffect, useState, useRef, createContext, lazy } from 'react';
import { useNavigate } from 'react-router-dom';
import './tailwind.css'
import { cleanLocalStorage } from './helpers/helper';
const ConfirmPopup = lazy(() => import('./helpers/common/modals/ConfirmPopup'));

export const MyContext = createContext("");

function App() {
  const [login, setLogin] = useState("");
  const [isActive, setIsActive] = useState(true);
  const [showPrompt, setShowPrompt] = useState(false);
  const timeoutId = useRef(null);
  const navigate = useNavigate();

  const resetTimer = () => {
    if (showPrompt) {
      return;
    }
    setIsActive(true);
    setShowPrompt(false);
    clearTimeout(timeoutId.current);
    if (login) {
      timeoutId.current = setTimeout(() => {
        setIsActive(false);
        setShowPrompt(true);
      }, 13 * 60 * 1000); // 1 minute = 60000 milliseconds
    }
  };

  // Function to handle user activity
  const handleActivity = () => {
    if (!isActive) {
      resetTimer();
    }
  };

  const logout = () => {
    setShowPrompt(false);
    cleanLocalStorage();
    navigate("/login");
    setLogin("");
  }

  useEffect(() => {
    const token = localStorage.getItem('token');

    if (token) {
      setLogin(token)
    }
  }, [login]);

  useEffect(() => {
    let timer
    clearTimeout(timer);
    if (showPrompt) {
      timer = setTimeout(() => {
        logout();
      }, 2 * 60 * 1000);
    }
    return () => {
      clearTimeout(timer);
    }

  }, [showPrompt])

  useEffect(() => {
    if (login) {
      window.addEventListener('mousemove', handleActivity);
      window.addEventListener('keydown', handleActivity);
      window.addEventListener('click', handleActivity);

      // Start the timer on mount
      resetTimer();

      // Clean up event listeners on unmount
      return () => {
        window.removeEventListener('mousemove', handleActivity);
        window.removeEventListener('keydown', handleActivity);
        window.removeEventListener('click', handleActivity);
        clearTimeout(timeoutId.current);
      };
    }
    setIsActive(false);
  }, [login]);

  return (
    <MyContext.Provider value={{ login, setLogin }} >
      <div>
       {/* You can update this model component with your own UI */}
        <ConfirmPopup
          open={showPrompt}
          setOpen={setShowPrompt}
          setAccepted={resetTimer}
          message={"You will be logged out soon due to inactivity. Click 'Continue' to stay logged in."}
          handleNo={logout}
          yesBtn={"Continue"}
          hideNoBtn={true}
        />
      </div>
    </MyContext.Provider >
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Conclusion

Tracking user inactivity is a powerful way to improve both security and user engagement on your website. By setting up timeouts, listening for user interactions, and displaying a thoughtful inactivity prompt, you can ensure that your users have the best experience possible while keeping their sessions secure.

This approach can be extended to a wide variety of use cases, such as auto-saving data, preventing session hijacking, or prompting users to complete tasks before they leave your site. Experiment with different timeout periods, messaging, and prompts to find what works best for your application.

Top comments (0)