useIdle

Medium

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

Hint 1

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;
}
Hint 2

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:

  1. It sets the idle state to false because the user is now active
  2. It clears any existing timeout to prevent multiple timers running simultaneously
  3. 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.

00:00