Scroll-driven UI — a progress bar, a shrinking header, a "back to top" button, parallax — needs the current scroll offset in React state. The naive onScroll → setState works but fires dozens of times per second, re-rendering on every pixel and janking the scroll. The fix is two well-known techniques: mark the listener passive (so it can't block scrolling) and throttle with requestAnimationFrame (so you update at most once per frame). useScrollPosition bundles both.
Implement useScrollPosition(). Return { x, y } for window.scrollX/scrollY, seeded from the current position, updated on a passive scroll listener that's throttled to one state update per animation frame, and cleaned up on unmount.
function useScrollPosition() {
// returns { x, y }
}
const { y } = useScrollPosition();
const progress = y / (document.body.scrollHeight - window.innerHeight);
<ProgressBar value={progress} />
const { y } = useScrollPosition();
const showBackToTop = y > 600;
window.scrollX/Y (0 on the server) so the first render isn't wrong.{ passive: true } so the browser knows you won't preventDefault, keeping scrolling smooth.requestAnimationFrame so state updates at most once per frame, reading the latest position when the frame runs.