From a737f11b50fdf84ee505e793fde92af8d3a2390f Mon Sep 17 00:00:00 2001 From: agatha197 <28584164+agatha197@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:14:33 +0000 Subject: [PATCH] feat(FR-1530): Implement window focus check for automatic refresh interval (#4358) Resolves #4357 ([FR-1530](https://lablup.atlassian.net/browse/FR-1530)) # Add pauseWhenHidden option to interval hooks This PR adds a `pauseWhenHidden` option to the `useInterval` and `useIntervalValue` hooks, allowing intervals to be paused when the page is hidden (tab not in focus). The feature is enabled by default but can be disabled by setting `pauseWhenHidden={false}`. When enabled: - Intervals pause when the page is hidden - Callbacks execute immediately when the page becomes visible again - Reduces unnecessary background processing The `BAIFetchKeyButton` component has been updated to support this new option, with the default behavior maintaining backward compatibility. **Checklist:** - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after [FR-1530]: https://lablup.atlassian.net/browse/FR-1530?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- react/src/components/BAIFetchKeyButton.tsx | 4 ++ react/src/hooks/useIntervalValue.tsx | 67 +++++++++++++++++----- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/react/src/components/BAIFetchKeyButton.tsx b/react/src/components/BAIFetchKeyButton.tsx index e22d837e55..fb831f7af0 100644 --- a/react/src/components/BAIFetchKeyButton.tsx +++ b/react/src/components/BAIFetchKeyButton.tsx @@ -16,6 +16,7 @@ interface BAIAutoRefetchButtonProps size?: ButtonProps['size']; onChange: (fetchKey: string) => void; hidden?: boolean; + pauseWhenHidden?: boolean; } const BAIFetchKeyButton: React.FC = ({ value, @@ -26,6 +27,7 @@ const BAIFetchKeyButton: React.FC = ({ size, hidden, lastLoadTime: lastLoadTimeProp, + pauseWhenHidden = true, ...buttonProps }) => { const { t } = useTranslation(); @@ -65,6 +67,7 @@ const BAIFetchKeyButton: React.FC = ({ }, showLastLoadTime ? 5_000 : null, lastLoadTime.toISOString(), + pauseWhenHidden, ); // remember when loading is done to display when the last fetch was done @@ -80,6 +83,7 @@ const BAIFetchKeyButton: React.FC = ({ }, // only start auto-updating after the previous loading is false(done). loading ? null : autoUpdateDelay, + pauseWhenHidden, ); const tooltipTitle = showLastLoadTime ? loadTimeMessage : undefined; diff --git a/react/src/hooks/useIntervalValue.tsx b/react/src/hooks/useIntervalValue.tsx index e669d83b6d..58c7c32fb9 100644 --- a/react/src/hooks/useIntervalValue.tsx +++ b/react/src/hooks/useIntervalValue.tsx @@ -5,24 +5,57 @@ import { useEffect, useRef, useState } from 'react'; * * @param callback The function to be executed at the specified interval. * @param delay The delay (in milliseconds) between each execution of the callback function. If `null`, the interval is cleared(pause). + * @param pauseWhenHidden Whether to pause the interval when the page becomes hidden. Defaults to true. */ -export function useInterval(callback: () => void, delay: number | null) { - const savedCallback = useRef<() => any>(null); +export function useInterval( + callback: () => void, + delay: number | null, + pauseWhenHidden: boolean = true, +) { + const savedCallback = useRef<(() => void) | null>(null); + const [isPageVisible, setIsPageVisible] = useState(() => + typeof document === 'undefined' ? true : !document.hidden, + ); useEffect(() => { savedCallback.current = callback; }); + // Handle page visibility changes + useEffect(() => { + if (!pauseWhenHidden || typeof window === 'undefined') return; + + const handleVisibilityChange = () => { + const visible = !document.hidden; + setIsPageVisible((prev) => { + if (delay !== null && !prev && visible) { + // Execute callback immediately when page becomes visible again + savedCallback.current?.(); + } + return visible; + }); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [pauseWhenHidden, delay]); + useEffect(() => { function tick() { savedCallback.current?.(); } if (delay !== null) { - let id = setInterval(tick, delay); - return () => clearInterval(id); + // Only check visibility if pauseWhenHidden is enabled + if (!pauseWhenHidden || isPageVisible) { + let id = setInterval(tick, delay); + return () => clearInterval(id); + } } - }, [delay]); + }, [delay, pauseWhenHidden, isPageVisible]); } /** @@ -31,14 +64,16 @@ export function useInterval(callback: () => void, delay: number | null) { * @param calculator - A function that calculates the value. * @param delay - The delay in milliseconds between updates. * @param triggerKey - An optional key that triggers an immediate update when changed. + * @param pauseWhenHidden - Whether to pause the interval when the page becomes hidden. Defaults to true. * @returns The updated value. */ -export const useIntervalValue = ( - calculator: () => any, +export function useIntervalValue( + calculator: () => T, delay: number | null, triggerKey?: string, -) => { - const [result, setResult] = useState(calculator()); + pauseWhenHidden: boolean = true, +): T { + const [result, setResult] = useState(calculator()); useEffect(() => { if (triggerKey) { @@ -47,10 +82,14 @@ export const useIntervalValue = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [triggerKey]); - useInterval(() => { - const newResult = calculator(); - if (newResult !== result) setResult(newResult); - }, delay); + useInterval( + () => { + const newResult = calculator(); + if (newResult !== result) setResult(newResult); + }, + delay, + pauseWhenHidden, + ); return result; -}; +}