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:
2026-04-29 20:01:25 +02:00
parent 0a350b3acc
commit 336242ad06
5 changed files with 243 additions and 34 deletions

View File

@@ -2,7 +2,7 @@
* React hook for polling blocklist schedule error state.
*/
import { useEffect, useRef, useState } from "react";
import { usePolledData } from "./usePolledData";
import { fetchSchedule } from "../api/blocklist";
const BLOCKLIST_POLL_INTERVAL_MS = 60_000;
@@ -14,36 +14,17 @@ export interface UseBlocklistStatusReturn {
/**
* Poll `GET /api/blocklists/schedule` every 60 seconds to detect whether
* the most recent blocklist import had errors.
*
* Polling pauses when the page is hidden and resumes immediately when visible.
*/
export function useBlocklistStatus(): UseBlocklistStatusReturn {
const [hasErrors, setHasErrors] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const { data } = usePolledData({
fetcher: (signal) => fetchSchedule(signal),
selector: (response) => response.last_run_errors === true,
errorMessage: "Failed to fetch blocklist schedule",
pollInterval: BLOCKLIST_POLL_INTERVAL_MS,
pauseWhenHidden: true,
});
useEffect(() => {
const poll = (): void => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
fetchSchedule(controller.signal)
.then((info) => {
if (controller.signal.aborted) {
return;
}
setHasErrors(info.last_run_errors === true);
})
.catch(() => {
// Silently swallow network errors — do not change indicator state.
});
};
poll();
const id = window.setInterval(poll, BLOCKLIST_POLL_INTERVAL_MS);
return (): void => {
abortRef.current?.abort();
window.clearInterval(id);
};
}, []);
return { hasErrors };
return { hasErrors: data ?? false };
}