useIdle
Prompt
Create a custom React hook called useIdle that detects when a user has been inactive for a specified period of time.
This hook is useful for implementing features like automatic logout, displaying inactivity notifications, or adjusting UI elements based on user engagement.
Requirements
- The hook should accept a timeout parameter (in milliseconds) after which the user is considered idle
- The hook should return a boolean indicating whether the user is idle
Playground
Define the events array at module level and set up the state properly:
import { useState, useEffect } from 'react';
// Define these events at module level to prevent recreating on each render
const EVENTS = [
'mousemove',
'mousedown',
'keydown',
'touchstart',
'click',
'wheel',
'resize'
];
export function useIdle(ms = 10000) {
// Start with user in active state (not idle)
const [isIdle, setIsIdle] = useState(false);
useEffect(() => {}, [ms]);
return isIdle;
}Implement the core timer logic with a proper cleanup function:
useEffect(() => {
let timeoutId;
// Handler to reset the timer whenever user is active
const handleActivity = () => {
// User is active, so they're not idle
setIsIdle(false);
// Clear existing timeout and set a new one
clearTimeout(timeoutId);
timeoutId = setTimeout(() => setIsIdle(true), ms);
};
// Special handler for when user switches tabs and comes back
const handleVisibilityChange = () => {
if (!document.hidden) {
handleActivity();
}
};
// Add listeners for all user activity events
EVENTS.forEach(event => {
window.addEventListener(event, handleActivity);
});
// Handle tab/visibility changes
document.addEventListener('visibilitychange', handleVisibilityChange);
// Start the initial timer
timeoutId = setTimeout(() => setIsIdle(true), ms);
// Clean up all listeners and timers
return () => {
clearTimeout(timeoutId);
EVENTS.forEach(event => {
window.removeEventListener(event, handleActivity);
});
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [ms]);Using module-level constants and properly handling visibility changes makes this hook robust for real-world usage.
Solution
Explanation
The useIdle hook is a useful tool for detecting user inactivity in a React application. Let's break down how it works:
First, we define the events we want to listen for at the top level, outside the hook function:
const EVENTS = [
'mousemove',
'mousedown',
'keydown',
'touchstart',
'click',
'wheel',
'resize',
];These are common browser events that indicate user activity. By placing this outside the hook, we avoid recreating this array on every render.
Inside our hook, we create a state variable to track whether the user is idle:
const [isIdle, setIsIdle] = useState(false);Next, we set up an effect that will manage the idle timer and event listeners:
useEffect(() => {
let timeoutId;
// ...rest of the effect
}, [ms]);The dependency array [ms] ensures that this effect runs whenever the timeout duration changes.
The core of our hook is the handleActivity function:
const handleActivity = () => {
setIsIdle(false);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => setIsIdle(true), ms);
};This function does three important things:
- It sets the idle state to
falsebecause the user is now active - It clears any existing timeout to prevent multiple timers running simultaneously
- It sets a new timeout that will mark the user as idle after the specified duration (
ms) of inactivity
We also handle visibility changes, which is useful for scenarios where a user switches tabs and then returns:
const handleVisibilityChange = () => {
if (!document.hidden) {
handleActivity();
}
};This ensures that when a user returns to your application after using a different tab, we properly reset the idle timer.
We attach our event listeners to the appropriate targets:
EVENTS.forEach((event) => {
window.addEventListener(event, handleActivity);
});
document.addEventListener(
'visibilitychange',
handleVisibilityChange
);And we start the initial idle timer:
timeoutId = setTimeout(() => setIsIdle(true), ms);Finally, we ensure proper cleanup when the component unmounts:
return () => {
clearTimeout(timeoutId);
EVENTS.forEach((event) => {
window.removeEventListener(event, handleActivity);
});
document.removeEventListener(
'visibilitychange',
handleVisibilityChange
);
};This is crucial for preventing memory leaks and ensuring that the event listeners and timers don't continue running after the component is no longer in the DOM.