Implement visibility-aware polling to reduce background tab resource usage
- Add usePageVisibility hook to track page visibility state - Add pauseWhenHidden option to usePolledData (defaults to false for backward compatibility) - When enabled, polling pauses when page is hidden and resumes with immediate refresh when visible - Refactor useBlocklistStatus to use usePolledData with pauseWhenHidden=true - Add comprehensive tests for usePageVisibility hook - Add polling lifecycle documentation to Web-Development.md Fixes #36: Polling continues when tab is not visible Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useFetchData } from "./useFetchData";
|
||||
import { usePageVisibility } from "./usePageVisibility";
|
||||
import type { FetchError } from "../types/api";
|
||||
|
||||
export interface UsePolledDataOptions<TResponse, TData> {
|
||||
@@ -23,6 +24,12 @@ export interface UsePolledDataOptions<TResponse, TData> {
|
||||
pollInterval?: number;
|
||||
/** If true, automatically refetch when browser window regains focus. Defaults to true. */
|
||||
refetchOnWindowFocus?: boolean;
|
||||
/**
|
||||
* If true, pause polling when the page is hidden (user switched to another tab).
|
||||
* Polling resumes immediately when the page becomes visible, with an immediate refresh.
|
||||
* Defaults to false for backward compatibility.
|
||||
*/
|
||||
pauseWhenHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface UsePolledDataResult<TData> {
|
||||
@@ -47,6 +54,9 @@ export interface UsePolledDataResult<TData> {
|
||||
* - `"network_error"`: Network, DNS, or JSON parse failure
|
||||
* - `"abort_error"`: Request was cancelled (typically silently ignored by hook)
|
||||
*
|
||||
* When `pauseWhenHidden` is enabled, polling pauses when the page becomes hidden
|
||||
* and resumes immediately with a fresh fetch when the page becomes visible again.
|
||||
*
|
||||
* @param options - Configuration options
|
||||
* @returns Data, loading state, typed error, and refresh callback
|
||||
*/
|
||||
@@ -61,6 +71,7 @@ export function usePolledData<TResponse, TData>(
|
||||
initialData,
|
||||
pollInterval,
|
||||
refetchOnWindowFocus = true,
|
||||
pauseWhenHidden = false,
|
||||
} = options;
|
||||
|
||||
const { data, loading, error, refresh } = useFetchData({
|
||||
@@ -72,25 +83,61 @@ export function usePolledData<TResponse, TData>(
|
||||
});
|
||||
|
||||
const refreshRef = useRef(refresh);
|
||||
const intervalIdRef = useRef<number | null>(null);
|
||||
const previousVisibilityRef = useRef<boolean | null>(null);
|
||||
const isVisible = usePageVisibility();
|
||||
|
||||
useEffect(() => {
|
||||
refreshRef.current = refresh;
|
||||
}, [refresh]);
|
||||
|
||||
// Polling effect: set up interval if pollInterval is provided
|
||||
// Polling effect: set up interval if pollInterval is provided and page is visible
|
||||
useEffect(() => {
|
||||
if (!pollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setInterval((): void => {
|
||||
// If pauseWhenHidden is enabled and page is hidden, clear any existing interval
|
||||
if (pauseWhenHidden && !isVisible) {
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const id = window.setInterval((): void => {
|
||||
refreshRef.current();
|
||||
}, pollInterval);
|
||||
|
||||
intervalIdRef.current = id;
|
||||
|
||||
return (): void => {
|
||||
clearInterval(id);
|
||||
if (intervalIdRef.current === id) {
|
||||
clearInterval(id);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [pollInterval]);
|
||||
}, [pollInterval, pauseWhenHidden, isVisible]);
|
||||
|
||||
// Visibility effect: handle pause/resume when page visibility changes
|
||||
useEffect(() => {
|
||||
if (!pauseWhenHidden || !pollInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only refresh if this is a transition from hidden to visible, not on mount
|
||||
if (previousVisibilityRef.current === false && isVisible) {
|
||||
// Page became visible: clear pending interval and refresh immediately
|
||||
if (intervalIdRef.current !== null) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
refreshRef.current();
|
||||
}
|
||||
|
||||
previousVisibilityRef.current = isVisible;
|
||||
}, [isVisible, pauseWhenHidden, pollInterval]);
|
||||
|
||||
// Window focus: optional refetch on regain focus
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user