Medium

useIdle

Prompt

Create a custom hook useIdle that detects when a user has been inactive for a specified duration. It should accept a timeout in milliseconds and return a boolean: true when the user is idle, false when active. Any user interaction (mouse, keyboard, touch, scroll) should reset the timer.

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 idea is straightforward once you see it: start a countdown timer. Every time the user does something (moves the mouse, presses a key, taps, scrolls), restart the timer. If the timer ever reaches zero without being interrupted, the user hasn't done anything for a while, so they're idle.

The events list

const EVENTS = [
'mousemove',
'mousedown',
'keydown',
'touchstart',
'click',
'wheel',
'resize',
];

These are the browser events that mean "the user is here and doing something." We define this array outside the hook function so it's created once and shared across all renders.

The heart of the hook

const handleActivity = () => {
setIsIdle(false);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => setIsIdle(true), ms);
};

Every time any of those events fires, three things happen: we mark the user as active (setIsIdle(false)), we cancel the old countdown (clearTimeout), and we start a fresh one (setTimeout). If the user keeps interacting, the countdown keeps resetting and never reaches zero. The moment they stop, the last countdown runs out and marks them as idle.

The setup

Inside useEffect, we attach handleActivity to every event in our list, kick off the initial timer, and return a cleanup function that tears everything down when the component unmounts. We also listen for visibilitychange so that when a user switches tabs and comes back, we treat that as activity and reset the timer.

The cleanup part is crucial. Without it, you'd have orphaned event listeners and timers running in the background after the component is gone, which causes memory leaks and weird bugs.

In real apps, this hook powers auto-logout after inactivity (like banking apps), "are you still watching?" prompts (like Netflix), and pausing expensive animations when the user isn't looking.