Page visibility tells you whether the current document is visible to the user or hidden in a background tab. Build usePageVisibility so a component can pause expensive work while hidden and resume it when visible again. The hook reads the Page Visibility API, reacts to its document event, and remains safe when the API is unavailable.
function usePageVisibility(
defaultValue?: 'visible' | 'hidden' // defaults to 'visible'
): 'visible' | 'hidden';
function VideoPlayer() {
const visibility = usePageVisibility();
return <video data-paused={visibility === 'hidden'} />;
}
// document.visibilityState === 'visible' -> 'visible'
// visibilitychange after the tab is hidden -> 'hidden'
// The API is unavailable, so the fallback is returned.
usePageVisibility(); // 'visible'
usePageVisibility('hidden'); // 'hidden'
document.visibilityState is available, return its current 'visible' or 'hidden' value even when that differs from defaultValue.document's visibilitychange event and read document.visibilityState inside the handler; do not reuse the mount-time value.document or document.visibilityState is unavailable, return defaultValue without throwing.blur or focus events.You will keep one React state value synchronized with the browser's current page-visibility signal.
A video player can stop rendering frames when its tab is hidden. An analytics dashboard can postpone refreshes until the user returns. Both need the document's real visibility state, not a guess based on whether the window has focus. Your hook reads that state, listens for the browser's visibility event, and removes the listener when its component leaves the page.
Treat document.visibilityState as the source of truth and React state as a copy that drives rendering. The browser changes the source first, then dispatches visibilitychange; the event is your signal to read the source again.
const { useState } = require('react');
function usePageVisibility(defaultValue = 'visible') {
const [visibility] = useState(() => {
if (typeof document === 'undefined') return defaultValue;
return document.visibilityState || defaultValue;
});
return visibility;
}
This version is safe and returns the right initial value. But a state initializer runs only during the first render. When the browser later changes from 'visible' to 'hidden', nothing tells React to read the document again, so the returned value stays stale.
const { useEffect, useRef, useState } = require('react');
function readVisibility(fallback) {
if (typeof document === 'undefined') return fallback;
const value = document.visibilityState;
return value === 'visible' || value === 'hidden' ? value : fallback;
}
function usePageVisibility(defaultValue = 'visible') {
const defaultRef = useRef(defaultValue);
defaultRef.current = defaultValue;
const [visibility, setVisibility] = useState(() =>
readVisibility(defaultValue)
);
useEffect(() => {
if (typeof document === 'undefined') return undefined;
// Read inside the handler because the document can change after mount.
const handleVisibilityChange = () => {
setVisibility(readVisibility(defaultRef.current));
};
document.addEventListener('visibilitychange', handleVisibilityChange);
// The document wins if it changed between render and effect setup.
handleVisibilityChange();
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return visibility;
}
module.exports = { usePageVisibility };
readVisibility narrows every result to the hook's two allowed strings and falls back safely when no document signal exists. The effect creates one handler, adds that exact function to the document, and returns a cleanup that removes it. defaultRef keeps a later fallback value available to the stable handler without resubscribing on every render.
Start with a visible tab and call usePageVisibility('hidden').
readVisibility('hidden') sees document.visibilityState === 'visible', so the hook returns 'visible'. The real document value wins over the fallback.visibilitychange handler to document. It immediately runs the handler once, closing the small render-to-effect timing gap.document.visibilityState to 'hidden' and dispatches visibilitychange.'hidden' at that moment and calls setVisibility('hidden'). React rerenders the component with the new result.removeEventListener, so later visibility changes cannot update this hook instance.visibilitychange and read again inside its handler.window. The Page Visibility API dispatches visibilitychange on document; a window blur can also happen while the page remains visible. Fix: use the document event and its state.removeEventListener('visibilitychange', () => update()) cannot remove the function originally added. Fix: define one handler and pass the same reference to both calls.document, and some environments omit visibilityState. Fix: guard the read and return the caller's fallback.useSyncExternalStore. Model document visibility as an external browser store when you need a server snapshot and React's subscription contract in a shared utility.pagehide, pageshow, or the Page Lifecycle API when freezing and back-forward cache restoration matter; those events answer different questions from visibility alone.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Page visibility tells you whether the current document is visible to the user or hidden in a background tab. Build usePageVisibility so a component can pause expensive work while hidden and resume it when visible again. The hook reads the Page Visibility API, reacts to its document event, and remains safe when the API is unavailable.
function usePageVisibility(
defaultValue?: 'visible' | 'hidden' // defaults to 'visible'
): 'visible' | 'hidden';
function VideoPlayer() {
const visibility = usePageVisibility();
return <video data-paused={visibility === 'hidden'} />;
}
// document.visibilityState === 'visible' -> 'visible'
// visibilitychange after the tab is hidden -> 'hidden'
// The API is unavailable, so the fallback is returned.
usePageVisibility(); // 'visible'
usePageVisibility('hidden'); // 'hidden'
document.visibilityState is available, return its current 'visible' or 'hidden' value even when that differs from defaultValue.document's visibilitychange event and read document.visibilityState inside the handler; do not reuse the mount-time value.document or document.visibilityState is unavailable, return defaultValue without throwing.blur or focus events.You will keep one React state value synchronized with the browser's current page-visibility signal.
A video player can stop rendering frames when its tab is hidden. An analytics dashboard can postpone refreshes until the user returns. Both need the document's real visibility state, not a guess based on whether the window has focus. Your hook reads that state, listens for the browser's visibility event, and removes the listener when its component leaves the page.
Treat document.visibilityState as the source of truth and React state as a copy that drives rendering. The browser changes the source first, then dispatches visibilitychange; the event is your signal to read the source again.
const { useState } = require('react');
function usePageVisibility(defaultValue = 'visible') {
const [visibility] = useState(() => {
if (typeof document === 'undefined') return defaultValue;
return document.visibilityState || defaultValue;
});
return visibility;
}
This version is safe and returns the right initial value. But a state initializer runs only during the first render. When the browser later changes from 'visible' to 'hidden', nothing tells React to read the document again, so the returned value stays stale.
const { useEffect, useRef, useState } = require('react');
function readVisibility(fallback) {
if (typeof document === 'undefined') return fallback;
const value = document.visibilityState;
return value === 'visible' || value === 'hidden' ? value : fallback;
}
function usePageVisibility(defaultValue = 'visible') {
const defaultRef = useRef(defaultValue);
defaultRef.current = defaultValue;
const [visibility, setVisibility] = useState(() =>
readVisibility(defaultValue)
);
useEffect(() => {
if (typeof document === 'undefined') return undefined;
// Read inside the handler because the document can change after mount.
const handleVisibilityChange = () => {
setVisibility(readVisibility(defaultRef.current));
};
document.addEventListener('visibilitychange', handleVisibilityChange);
// The document wins if it changed between render and effect setup.
handleVisibilityChange();
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return visibility;
}
module.exports = { usePageVisibility };
readVisibility narrows every result to the hook's two allowed strings and falls back safely when no document signal exists. The effect creates one handler, adds that exact function to the document, and returns a cleanup that removes it. defaultRef keeps a later fallback value available to the stable handler without resubscribing on every render.
Start with a visible tab and call usePageVisibility('hidden').
readVisibility('hidden') sees document.visibilityState === 'visible', so the hook returns 'visible'. The real document value wins over the fallback.visibilitychange handler to document. It immediately runs the handler once, closing the small render-to-effect timing gap.document.visibilityState to 'hidden' and dispatches visibilitychange.'hidden' at that moment and calls setVisibility('hidden'). React rerenders the component with the new result.removeEventListener, so later visibility changes cannot update this hook instance.visibilitychange and read again inside its handler.window. The Page Visibility API dispatches visibilitychange on document; a window blur can also happen while the page remains visible. Fix: use the document event and its state.removeEventListener('visibilitychange', () => update()) cannot remove the function originally added. Fix: define one handler and pass the same reference to both calls.document, and some environments omit visibilityState. Fix: guard the read and return the caller's fallback.useSyncExternalStore. Model document visibility as an external browser store when you need a server snapshot and React's subscription contract in a shared utility.pagehide, pageshow, or the Page Lifecycle API when freezing and back-forward cache restoration matter; those events answer different questions from visibility alone.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.