Hookipedia

usePageVisibility

A high-performance React hook that tracks document visibility status.

Introduction

The usePageVisibility hook allows components to track whether the current page is visible to the user or hidden (e.g., when the user switches tabs or minimizes the window). It leverages the Page Visibility API through React's useSyncExternalStore, ensuring zero "tearing" and optimal performance by subscribing only to browser-level events.

Basic Usage

import { useEffect, useRef } from 'react';
import { usePageVisibility } from '@/hooks/usePageVisibility';

function VideoPlayer() {
  const isVisible = usePageVisibility();
  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    if (isVisible) {
      videoRef.current?.play();
    } else {
      videoRef.current?.pause();
    }
  }, [isVisible]);

  return (
    <video ref={videoRef} muted loop autoPlay playsInline>
      <source src="/promo.mp4" />
    </video>
  );
}

API Reference

Returns

boolean

Returns true if the document is currently visible (document.visibilityState === 'visible'), and false otherwise.

Hook

usePageVisibility.ts
'use client';

import { useSyncExternalStore } from 'react';

const subscribe = (onStoreChange: () => void) => {
  document.addEventListener('visibilitychange', onStoreChange);
  return () => {
    document.removeEventListener('visibilitychange', onStoreChange);
  };
};

const getSnapshot = () => {
  return !document.hidden;
};

// Server snapshot: static value used during SSR
// We default to 'true' (Visible) because most users load a page to view it.
// Defaulting to 'false' would cause a hydration mismatch on almost every load.
const getServerSnapshot = () => {
  return true;
};

/**
 * A hook that tracks the document's visibility state.
 *
 * @returns `true` if the page is visible, `false` otherwise.
 */
export function usePageVisibility() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

Advanced Examples

Resource-Safe Polling

This example demonstrates how to stop expensive background data fetching when the user isn't looking at the page, saving both battery and server bandwidth.

import { useEffect } from 'react';
import { usePageVisibility } from '@/hooks/usePageVisibility';

function Dashboard({ stockSymbol }) {
  const isVisible = usePageVisibility();

  useEffect(() => {
    // If the tab is hidden, we don't start the interval at all.
    if (!isVisible) return;

    const interval = setInterval(() => {
      console.log(`Fetching live price for ${stockSymbol}...`);
    }, 3000);

    return () => clearInterval(interval);
  }, [isVisible, stockSymbol]);

  return <div>Monitoring {stockSymbol}...</div>;
}

Smart Document Title

We can use this hook to grab a user's attention when they navigate away.

import { useEffect, useRef } from 'react';
import { usePageVisibility } from '@/hooks/usePageVisibility';

function NotificationManager() {
  const isVisible = usePageVisibility();
  const originalTitle = useRef<string | null>(null);

  useEffect(() => {
    // Capture the REAL title only the first time the component mounts
    if (originalTitle.current === null) {
      originalTitle.current = document.title;
    }

    if (!isVisible) {
      document.title = "Come back soon! 👋";
    } else {
      document.title = originalTitle.current;
    }
  }, [isVisible]);

  return null;
}

Why use this?

Reliable State Synchronization

Traditional implementations often use useEffect and useState, which can lead to "state tearing" or hydration mismatches. By using a specialized synchronization pattern, we achieve several architectural benefits:

Sub-microsecond Sync

Because we subscribe directly to the browser's visibilitychange event, the UI updates the moment the user switches tabs. There is no scheduling delay, making the app feel incredibly responsive.

Predictable Hydration

By defaulting to true on the server, we ensure the HTML generated by the server matches what the browser expects on initial load. This prevents the "Flash of Hidden Content" (FOHC) where a component thinks it's hidden for a split second before the JS kicks in.

Automatic Cleanup

The subscription logic is completely encapsulated. When the last component using this hook unmounts, the browser event listener is stripped away automatically, ensuring zero memory leaks.

Error Handling

Environment Support

This hook relies on the document object. It is strictly a client-side hook and must be used in environments where the browser DOM is available.

  • Server Components: This hook uses the 'use client' directive. If you attempt to use it inside a React Server Component (RSC), Next.js will throw an error.
  • Iframe Constraints: In some browser configurations, if your app is running inside a cross-origin iframe, the Page Visibility API may be restricted for privacy reasons.
  • Hydration: If you have a specific use case where the page must start as hidden (e.g., pre-rendered hidden tabs), you may need to adjust the getServerSnapshot to avoid a hydration mismatch.

Last updated on

On this page

Edit this page on GitHub