React 19 hooks for user presence detection - tab visibility, window focus, online status, idle detection, page leave, wake lock, network info, heartbeat, and devtools detection
npm install @classytic/react-presencereact >= 19.0.0.
npm install @classytic/react-presence
`
Hooks
| Hook | Purpose |
|------|---------|
| usePresence | Combined presence -- composes tab, focus, online, idle, network into one status |
| useTabVisibility | Tab visible/hidden via Page Visibility API |
| useWindowFocus | Window focus/blur |
| useOnlineStatus | Browser online/offline |
| useIdleDetection | User inactivity with prompt stage, cross-tab sync, sleep detection |
| usePageLeave | Page leave detection (beforeunload, pagehide, mobile visibility) |
| useWakeLock | Screen Wake Lock API -- prevent screen dimming |
| useNetworkInfo | Network Information API -- effective type, downlink, RTT |
| useDevToolsDetection | DevTools open detection via window size heuristic |
| useHeartbeat | Periodic server pings with auto-pause on presence status |
Quick start
$3
`tsx
import { usePresence } from "@classytic/react-presence";
function App() {
const { status, state, resetAll } = usePresence({
idleTimeout: 30_000,
promptTimeout: 10_000,
onStatusChange: (status) => console.log(status),
});
// status: 'active' | 'prompted' | 'idle' | 'away' | 'offline'
return {status};
}
`
$3
`tsx
import {
PresenceProvider,
usePresenceStatus,
useIsTabVisible,
} from "@classytic/react-presence";
function App() {
return (
);
}
// Only re-renders when status changes
function StatusBadge() {
const status = usePresenceStatus();
return {status};
}
`
$3
`tsx
import { useTabVisibility, useIdleDetection } from "@classytic/react-presence";
const { isVisible, hiddenDuration, resetDuration } = useTabVisibility({
onHidden: () => log("tab hidden"),
});
const { isIdle, isPrompted, getRemainingTime } = useIdleDetection({
idleTimeout: 60_000,
promptTimeout: 10_000,
onPrompt: () => showModal("Still there?"),
});
`
---
API reference
$3
offline > away > idle > prompted > active
| Status | Condition |
|--------|-----------|
| offline | Browser offline |
| away | Tab hidden or window unfocused |
| idle | No user activity for idleTimeout ms |
| prompted | No activity for idleTimeout - promptTimeout ms (pre-idle warning) |
| active | None of the above |
---
$3
Composes useTabVisibility, useWindowFocus, useOnlineStatus, useIdleDetection, and optionally useNetworkInfo.
`ts
// Options
interface UsePresenceOptions {
idleTimeout?: number; // Default: 60000
promptTimeout?: number; // Pre-idle warning window (ms)
idleEvents?: string[]; // Default: mousemove, mousedown, keydown, touchstart, scroll, wheel
trackIdle?: boolean; // Default: true
trackNetwork?: boolean; // Default: false
crossTab?: boolean | CrossTabOptions;
detectSleep?: boolean | SleepDetectorOptions;
timers?: TimerFunctions; // Custom timers (e.g. worker timers)
onStatusChange?: (status: PresenceStatus, state: PresenceState) => void;
onTabVisibilityChange?: (state: TabVisibilityState) => void;
onWindowFocusChange?: (state: WindowFocusState) => void;
onOnlineStatusChange?: (state: OnlineState) => void;
onIdleChange?: (state: IdleState) => void;
onPrompt?: () => void;
onNetworkChange?: (state: NetworkInfoState) => void;
onSleep?: (event: SleepEvent) => void;
}
// Return
interface UsePresenceReturn {
status: PresenceStatus;
state: PresenceState;
tabVisibility: UseTabVisibilityReturn;
windowFocus: UseWindowFocusReturn;
onlineStatus: UseOnlineStatusReturn;
idle: UseIdleDetectionReturn;
network?: NetworkInfoState;
resetAll: () => void;
getRemainingTime: () => number; // ms until idle
getElapsedTime: () => number; // ms since last activity
activate: () => void; // reset from prompted/idle to active
}
`
---
$3
Tracks tab visibility via document.visibilityState. Live hiddenDuration counter updates every 1s while hidden.
`ts
// Options
interface UseTabVisibilityOptions {
onVisible?: () => void;
onHidden?: () => void;
onChange?: (state: TabVisibilityState) => void;
}
// Return
interface UseTabVisibilityReturn {
isVisible: boolean;
visibilityState: 'visible' | 'hidden';
lastChange: number; // timestamp
hiddenDuration: number; // cumulative ms hidden
resetDuration: () => void; // reset counter (safe to call while hidden)
}
`
---
$3
Tracks window focus/blur. Live blurDuration counter updates every 1s while blurred.
`ts
// Options
interface UseWindowFocusOptions {
onFocus?: () => void;
onBlur?: () => void;
onChange?: (state: WindowFocusState) => void;
}
// Return
interface UseWindowFocusReturn {
isFocused: boolean;
lastChange: number;
blurDuration: number; // cumulative ms blurred
resetDuration: () => void; // safe to call while blurred
}
`
---
$3
Tracks navigator.onLine via online/offline events.
`ts
// Options
interface UseOnlineStatusOptions {
onOnline?: () => void;
onOffline?: () => void;
onChange?: (state: OnlineState) => void;
}
// Return
interface UseOnlineStatusReturn {
isOnline: boolean;
lastChange: number;
wasOffline: boolean; // true if ever went offline this session
resetOfflineFlag: () => void;
}
`
---
$3
Detects user inactivity. State machine: active -> prompted -> idle -> active.
Features: prompt stage, cross-tab sync, sleep/hibernate detection, worker timer support, imperative time getters.
`ts
// Options
interface UseIdleDetectionOptions {
idleTimeout?: number; // Default: 60000
promptTimeout?: number; // ms before idle to trigger prompt
events?: string[]; // Default: mousemove, mousedown, keydown, touchstart, scroll, wheel
startOnMount?: boolean; // Default: true
timers?: TimerFunctions; // Worker timers for background accuracy
crossTab?: boolean | CrossTabOptions;
detectSleep?: boolean | SleepDetectorOptions;
onIdle?: () => void;
onActive?: () => void;
onPrompt?: () => void;
onChange?: (state: IdleState) => void;
onSleep?: (event: SleepEvent) => void;
}
// Return
interface UseIdleDetectionReturn {
isIdle: boolean;
isPrompted: boolean;
idleTime: number; // ms since last activity (live, updates every 1s)
lastActivity: number; // timestamp
isRunning: boolean;
markActive: () => void; // reset to active
activate: () => void; // alias for markActive
start: () => void;
stop: () => void;
getRemainingTime: () => number; // imperative, ms until idle
getElapsedTime: () => number; // imperative, ms since last activity
getActiveTime: () => number; // imperative, total active ms
crossTab?: CrossTabState; // { isLeader, tabCount, lastActivityAcrossTabs }
}
`
---
$3
bfcache-compatible leave detection. Uses beforeunload, pagehide, visibility change (mobile). No deprecated unload event.
`ts
// Options
interface UsePageLeaveOptions {
preventLeave?: boolean | (() => boolean); // triggers browser "Leave site?" dialog
beaconUrl?: string; // send beacon on leave
beaconData?: () => BodyInit;
onBeforeLeave?: () => void;
onLeave?: () => void;
onChange?: (state: PageLeaveState) => void;
}
// Return
interface UsePageLeaveReturn {
isLeaving: boolean;
leaveAttempts: number;
lastLeaveAttempt: number;
enablePrevention: () => void; // dynamically enable leave prevention (sets override)
disablePrevention: () => void; // dynamically disable (sets override)
clearOverride: () => void; // clear override, return to prop-controlled behavior
}
`
Override behavior:
- enablePrevention() / disablePrevention() set an override that persists across re-renders
- Boolean prop changes (true → false) clear any active override
- Function props can be inline (new reference each render) without clearing override
- clearOverride() explicitly returns to prop-controlled behavior
---
$3
Screen Wake Lock API. Auto re-acquires on tab return (browser releases on tab hide).
`ts
// Options
interface UseWakeLockOptions {
autoReacquire?: boolean; // Default: true
onLock?: () => void;
onRelease?: () => void;
onError?: (error: Error) => void;
}
// Return
interface UseWakeLockReturn {
isSupported: boolean;
isLocked: boolean;
error: string | null;
request: () => Promise;
release: () => Promise;
}
`
---
$3
Network Information API (Chromium only, graceful no-op fallback).
`ts
// Options
interface UseNetworkInfoOptions {
enabled?: boolean; // Default: true. Set false to skip subscription.
onChange?: (state: NetworkInfoState) => void;
onSlow?: () => void; // connection became slow-2g or 2g
onRecover?: () => void; // connection recovered from slow
}
// Return
interface UseNetworkInfoReturn {
isSupported: boolean;
effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | null;
downlink: number | null; // Mbps
rtt: number | null; // ms
saveData: boolean;
isSlowConnection: boolean; // true if slow-2g or 2g
}
`
---
$3
Detects DevTools open state via outerWidth - innerWidth > threshold.
> Limitation: This is a best-effort heuristic. Cannot detect undocked DevTools (separate window) and may false-positive on narrow browser windows. Use for analytics/UX hints, not security.
`ts
// Options
interface UseDevToolsDetectionOptions {
checkInterval?: number; // Default: 1000
threshold?: number; // Default: 160 (px)
onOpen?: () => void;
onClose?: () => void;
onChange?: (state: DevToolsState) => void;
}
// Return
interface UseDevToolsDetectionReturn {
isOpen: boolean;
}
`
---
$3
Periodic server pings. Auto-pauses when presence status is idle/away/offline.
`ts
// Options (onPing is required)
interface UseHeartbeatOptions {
onPing: () => Promise;
onPingError?: (error: Error) => void;
interval?: number; // Default: 30000
jitter?: number; // 0-1 range, e.g. 0.2 = ±20% interval variance
startOnMount?: boolean; // Default: true
autoPause?: boolean; // Default: true
pauseOnStatus?: PresenceStatus[]; // Default: ['idle', 'away', 'offline']
presenceStatus?: PresenceStatus; // pass from usePresence
}
// Jitter prevents "thundering herd" when many clients reconnect simultaneously
useHeartbeat({ onPing, interval: 30000, jitter: 0.2 }); // 24-36s intervals
// Return
interface UseHeartbeatReturn {
isActive: boolean;
lastPing: number;
pingCount: number;
lastPingFailed: boolean;
start: () => void;
stop: () => void;
ping: () => Promise; // manual ping
}
`
---
Context provider and selectors
PresenceProvider wraps usePresence and exposes granular selectors via useSyncExternalStore. Components only re-render when their selected slice changes.
`tsx
{children}
`
PresenceProvider accepts all UsePresenceOptions as props.
$3
All require PresenceProvider ancestor. Throw if used outside.
`ts
// Full context
usePresenceContext(): UsePresenceReturn
// State slice selectors
usePresenceStatus(): PresenceStatus
useTabVisibilityState(): TabVisibilityState
useWindowFocusState(): WindowFocusState
useOnlineStatusState(): OnlineState
useIdleState(): IdleState
useNetworkInfoState(): NetworkInfoState | undefined
// Boolean selectors (most granular)
useIsTabVisible(): boolean
useIsWindowFocused(): boolean
useIsOnline(): boolean
useIsIdle(): boolean
useIsPrompted(): boolean
useIsSlowConnection(): boolean
`
---
Worker timers
Secondary entry point for unthrottled timers in background tabs. Uses inline Web Worker via Blob URL.
`ts
import { createWorkerTimers } from "@classytic/react-presence/timers";
const timers = createWorkerTimers();
// Pass to hooks
useIdleDetection({ timers, idleTimeout: 60_000 });
usePresence({ timers, idleTimeout: 60_000 });
// Cleanup when done
timers.dispose();
`
---
Debugging
`ts
import { enableDebug, disableDebug } from "@classytic/react-presence";
enableDebug(); // or localStorage.setItem('DEBUG_REACT_PRESENCE', 'true')
`
---
Types
All types are exported from the main entry point:
`ts
import type {
PresenceStatus, // 'active' | 'prompted' | 'idle' | 'away' | 'offline'
PresenceState,
TabVisibilityState,
WindowFocusState,
OnlineState,
IdleState,
PageLeaveState,
WakeLockState,
NetworkInfoState,
HeartbeatState,
DevToolsState,
TimerFunctions,
CrossTabState,
SleepEvent,
} from "@classytic/react-presence";
``