diff --git a/src/web/hooks/__tests__/useReload.jsx b/src/web/hooks/__tests__/useReload.jsx
new file mode 100644
index 0000000000..e3a33e64eb
--- /dev/null
+++ b/src/web/hooks/__tests__/useReload.jsx
@@ -0,0 +1,188 @@
+/* SPDX-FileCopyrightText: 2024 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/* eslint-disable react/prop-types */
+
+import {describe, test, expect, testing} from '@gsa/testing';
+
+import {act, fireEvent, render, screen} from 'web/utils/testing';
+
+import useReload from '../useReload';
+
+const TestComponent = ({reload, timeout}) => {
+ const [startTimer, clearTimer, isRunning] = useReload(reload, timeout);
+ return (
+ <>
+
+
+ {'' + isRunning}
+ >
+ );
+};
+
+const runTimers = async () => {
+ await act(async () => {
+ await testing.advanceTimersToNextTimerAsync();
+ });
+};
+
+describe('useTiming', () => {
+ test('should start a timer to reload', async () => {
+ testing.useFakeTimers();
+
+ const reload = testing.fn();
+ const timeout = testing.fn().mockImplementation(() => 900);
+
+ render();
+
+ const isRunning = screen.getByTestId('isRunning');
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+
+ fireEvent.click(screen.getByTestId('startTimer'));
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+
+ timeout.mockClear();
+
+ await runTimers();
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+ });
+
+ test('should reload forever', async () => {
+ testing.useFakeTimers();
+
+ const reload = testing.fn().mockResolvedValue();
+ const timeout = testing.fn().mockImplementation(() => 900);
+
+ render();
+
+ const isRunning = screen.getByTestId('isRunning');
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+
+ fireEvent.click(screen.getByTestId('startTimer'));
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+
+ timeout.mockClear();
+
+ await runTimers();
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+
+ timeout.mockClear();
+ reload.mockClear();
+
+ await runTimers();
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+ });
+
+ test('should not reload if loading fails', async () => {
+ testing.useFakeTimers();
+
+ const reload = testing.fn().mockRejectedValue();
+ const timeout = testing.fn().mockImplementation(() => 900);
+
+ render();
+
+ const isRunning = screen.getByTestId('isRunning');
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+
+ fireEvent.click(screen.getByTestId('startTimer'));
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+
+ timeout.mockClear();
+
+ await runTimers();
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+ });
+
+ test('should allow to cancel reload', async () => {
+ testing.useFakeTimers();
+
+ const reload = testing.fn().mockResolvedValue();
+ const timeout = testing.fn().mockImplementation(() => 900);
+
+ render();
+
+ const isRunning = screen.getByTestId('isRunning');
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+
+ fireEvent.click(screen.getByTestId('startTimer'));
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+
+ timeout.mockClear();
+
+ await runTimers();
+
+ expect(isRunning).toHaveTextContent('true');
+ expect(reload).toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+
+ timeout.mockClear();
+ reload.mockClear();
+
+ fireEvent.click(screen.getByTestId('clearTimer'));
+
+ await runTimers();
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+ });
+
+ test('should not start reload reload timer', async () => {
+ testing.useFakeTimers();
+
+ const reload = testing.fn();
+ const timeout = testing.fn();
+
+ render();
+
+ const isRunning = screen.getByTestId('isRunning');
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).not.toHaveBeenCalled();
+
+ fireEvent.click(screen.getByTestId('startTimer'));
+
+ expect(isRunning).toHaveTextContent('false');
+ expect(reload).not.toHaveBeenCalled();
+ expect(timeout).toHaveBeenCalledWith({isVisible: true});
+ });
+});
diff --git a/src/web/hooks/useReload.js b/src/web/hooks/useReload.js
new file mode 100644
index 0000000000..c950401ec6
--- /dev/null
+++ b/src/web/hooks/useReload.js
@@ -0,0 +1,47 @@
+/* SPDX-FileCopyrightText: 2024 Greenbone AG
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import {useCallback, useEffect} from 'react';
+
+import useTiming from 'web/hooks/useTiming';
+
+/**
+ * A hook to reload data considering the visibility change of the browser tab.
+ *
+ * @param {Function} reloadFunc Function to call when the timer fires
+ * @param {Function} timeoutFunc Function to get the timeout value from
+ * @returns Array of startTimer function, clearTimer function and boolean isRunning
+ */
+const useReload = (reloadFunc, timeoutFunc) => {
+ const timeout = useCallback(
+ () => timeoutFunc({isVisible: !document.hidden}),
+ [timeoutFunc],
+ );
+
+ const [startTimer, clearTimer, isRunning] = useTiming(reloadFunc, timeout);
+
+ const handleVisibilityChange = useCallback(() => {
+ const isVisible = !document.hidden;
+
+ if (isVisible) {
+ // browser tab is visible again
+ // restart timer to get a possible shorter interval as the remaining time
+
+ clearTimer();
+ startTimer();
+ }
+ }, [clearTimer, startTimer]);
+
+ useEffect(() => {
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, [handleVisibilityChange]);
+
+ return [startTimer, clearTimer, isRunning];
+};
+
+export default useReload;