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
'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
getServerSnapshotto avoid a hydration mismatch.
Last updated on
useOnMount
A semantic, SSR-safe hook to execute code exactly once during component mount. Eliminates the ambiguity of empty dependency arrays and prevents linter noise.
usePrefersReducedMotion
A React hook for detecting user motion preferences via the prefers-reduced-motion media query, optimized for performance and SSR safety using useSyncExternalStore.