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.
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);
-
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);
-
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
}
};
- 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();
}
};
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
}
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]);
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]);
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}
/>
- 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;
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)