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
false
because 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.