Skip to content

Instantly share code, notes, and snippets.

@cpitt
Created June 30, 2020 22:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cpitt/f98b774a776920393d2f0d88ee66127c to your computer and use it in GitHub Desktop.
Save cpitt/f98b774a776920393d2f0d88ee66127c to your computer and use it in GitHub Desktop.
useIdleTimer React hook
import { renderHook, act } from '@testing-library/react-hooks';
import useIdleTimer from './useIdleTimer';
import { fireEvent } from '@testing-library/react';
jest.useFakeTimers();
describe('useIdleTimer', function() {
it('should start timer when startIdleTimer is called', function() {
const { result } = renderHook(() => useIdleTimer());
act(() => {
result.current.startIdleTimer();
jest.advanceTimersByTime(1000);
});
expect(result.current.idleTime).toBeGreaterThan(0);
});
it('should return idleTimeoutWarning true if threshold exceeded false if not', function() {
const { result } = renderHook(() => useIdleTimer(5, 10));
act(() => {
result.current.startIdleTimer();
jest.advanceTimersByTime(1000);
});
expect(result.current.idleTimeoutWarning).toBe(false);
act(() => {
jest.advanceTimersByTime(6000);
});
expect(result.current.idleTimeoutWarning).toBe(true);
});
it('should return idleTimeout true if threshold exceeded false if not', function() {
const { result } = renderHook(() => useIdleTimer(5, 10));
act(() => {
result.current.startIdleTimer();
jest.advanceTimersByTime(1000);
});
expect(result.current.idleTimeout).toBe(false);
act(() => {
jest.advanceTimersByTime(11000);
});
expect(result.current.idleTimeout).toBe(true);
});
it('should reset timer if timeout thresholds not met on active event', function() {
const { result } = renderHook(() => useIdleTimer(5, 10));
act(() => {
result.current.startIdleTimer();
jest.advanceTimersByTime(1000);
fireEvent.mouseMove(window);
});
expect(result.current.idleTime).toBe(0);
act(() => {
jest.advanceTimersByTime(10001);
fireEvent.mouseMove(window);
});
expect(result.current.idleTime).toBe(10);
});
it('should reset timer', function() {
const { result } = renderHook(() => useIdleTimer(5, 10));
act(() => {
result.current.startIdleTimer();
jest.advanceTimersByTime(1000);
});
expect(result.current.idleTime).toBe(1);
act(() => {
result.current.resetIdleTimer();
});
expect(result.current.idleTime).toBe(0);
});
it('should stop timer', function() {
const { result } = renderHook(() => useIdleTimer(5, 10));
act(() => {
result.current.startIdleTimer();
jest.advanceTimersByTime(1000);
});
expect(result.current.idleTime).toBe(1);
act(() => {
result.current.stopIdleTimer();
jest.advanceTimersByTime(1000);
});
expect(result.current.idleTime).toBe(1);
});
});
import { useEffect, useState } from 'react';
const events = [
'mousemove',
'keydown',
'wheel',
'DOMMouseScroll',
'mouseWheel',
'mousedown',
'touchstart',
'touchmove',
'MSPointerDown',
'MSPointerMove',
'visibilitychange',
];
/**
* creates an idle timer with warning and timeout threshold
* @param { number } warningThreshold time in seconds before
* trigging warning state defaults to 1500 (25 mins)
* @param { number } timeoutThreshold time in seconds before
* triggering timeout state change defaults 1800 (30 mins)
*/
const useIdleTimer = (
warningThreshold: number = 25 * 60,
timeoutThreshold: number = 30 * 60,
) => {
let idleInterval: NodeJS.Timer;
const [isRunning, setIsRunning] = useState(false);
const initState = {
idleTime: 0,
idleTimeoutWarning: false,
idleTimeout: false,
};
const [state, setState] = useState(initState);
const handleIdleInterval = () =>
setState(prevState => ({
idleTime: prevState.idleTime + 1,
idleTimeoutWarning:
prevState.idleTimeoutWarning || prevState.idleTime > warningThreshold,
idleTimeout:
prevState.idleTimeout || prevState.idleTime > timeoutThreshold,
}));
const startIdleTimer = () => setIsRunning(true);
const stopIdleTimer = () => setIsRunning(false);
const resetIdleTimer = () => setState({ ...initState });
const handleEvent = () => {
// Do not reset the timer if we've hit a warning or timeout
setState(prevState => ({
...prevState,
idleTime:
prevState.idleTimeout || prevState.idleTimeoutWarning
? prevState.idleTime
: 0,
}));
};
const tearDown = () => {
clearInterval(idleInterval);
events.forEach(event => {
window.removeEventListener(event, handleEvent);
});
};
const init = () => {
idleInterval = setInterval(handleIdleInterval, 1000);
events.forEach(event => {
window.addEventListener(event, handleEvent);
});
};
useEffect(() => {
if (isRunning) {
init();
} else {
tearDown();
}
return () => tearDown();
}, [isRunning]);
return {
...state,
startIdleTimer,
stopIdleTimer,
resetIdleTimer,
};
};
export default useIdleTimer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment