All files / hooks/use-page-visibility/src usePageVisibility.ts

88.23% Statements 15/17
50% Branches 1/2
75% Functions 6/8
87.5% Lines 14/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110                                                                                                                                                  17x 17x 11x     17x 10x       10x           5x 5x     10x   10x 10x       54x   17x         17x    
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import type { OnVisibilityChange, UsePageVisibilityReturn } from "./types";
import {
  SERVER_PAGE_VISIBILITY,
  getPageVisibility,
  isDocumentAvailable,
} from "./utils";
 
/**
 * Track whether the page (tab/window) is currently visible to the user, via the
 * [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API).
 *
 * Returns `true` while the page is in the foreground and `false` while it is
 * hidden — a background tab, a minimized window, or a locked/off screen. The
 * value updates on the document `visibilitychange` event.
 *
 * Built on `useSyncExternalStore`, so it is tear-free under concurrent
 * rendering and SSR-safe out of the box. On the server (or any environment
 * without a `document`) it returns `true`: the page is optimistically treated
 * as visible, which is what most apps want and avoids a hydration mismatch on
 * the first client paint of a foreground tab.
 *
 * Pass an optional `onChange` callback to run side effects on each transition
 * (pause a video, stop polling, mute audio, …). It is invoked from the
 * `visibilitychange` event handler — never from inside a `setState` updater —
 * and is read through a ref, so replacing the callback between renders never
 * re-subscribes the listener.
 *
 * @param onChange - Optional callback fired on every visibility transition with
 * the new boolean value (`true` = visible, `false` = hidden).
 * @returns `true` when the page is visible, `false` when hidden.
 *
 * @example
 * ```tsx
 * // Basic: show a badge while the tab is in the background
 * function TabStatus() {
 *   const visible = usePageVisibility();
 *   return <span>{visible ? "👀 Active" : "💤 Background"}</span>;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Pause work while hidden, resume when visible
 * function LiveFeed() {
 *   const visible = usePageVisibility();
 *   useEffect(() => {
 *     if (!visible) return; // don't poll in the background
 *     const id = setInterval(fetchUpdates, 5000);
 *     return () => clearInterval(id);
 *   }, [visible]);
 *   return <Feed />;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // React to transitions with the onChange callback
 * function Player({ videoRef }: { videoRef: React.RefObject<HTMLVideoElement> }) {
 *   usePageVisibility((visible) => {
 *     if (visible) videoRef.current?.play();
 *     else videoRef.current?.pause();
 *   });
 *   return <video ref={videoRef} />;
 * }
 * ```
 */
export function usePageVisibility(
  onChange?: OnVisibilityChange
): UsePageVisibilityReturn {
  // Latest-callback ref: hold the newest `onChange` without re-subscribing the
  // listener. Updated in a post-commit effect so we never fire a callback from
  // a render React may discard (StrictMode / concurrent safe).
  const onChangeRef = useRef<OnVisibilityChange | undefined>(onChange);
  useEffect(() => {
    onChangeRef.current = onChange;
  }, [onChange]);
 
  const subscribe = useCallback((onStoreChange: () => void) => {
    Iif (!isDocumentAvailable()) {
      return () => {};
    }
 
    const handleVisibilityChange = () => {
      // Notify the store first so React schedules a re-render with the new
      // value, then dispatch the user callback from the event handler —
      // outside any setState updater, so it is StrictMode/concurrent safe.
      // `visibilitychange` only fires on real transitions, so every call is a
      // genuine change.
      onStoreChange();
      onChangeRef.current?.(getPageVisibility());
    };
 
    document.addEventListener("visibilitychange", handleVisibilityChange);
 
    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, []);
 
  const getSnapshot = useCallback((): boolean => getPageVisibility(), []);
 
  const getServerSnapshot = useCallback(
    (): boolean => SERVER_PAGE_VISIBILITY,
    []
  );
 
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}