Community JavaScript Snippet
The Three Ref-Driven Scroll Hooks I Actually Use
Four ref recipes that earn their keep: `scrollIntoView`, deps-driven scroll-to-top, multi-target callback refs in a Map, and the `forwardRef` + `useImperativeHandle` escape hatch.
The Three Ref-Driven Scroll Hooks I Actually Use
Four ref recipes that earn their keep: `scrollIntoView`, deps-driven scroll-to-top, multi-target callback refs in a Map, and the `forwardRef` + `useImperativeHandle` escape hatch.
By @rajtanaka
November 26, 2025
·
Updated May 18, 2026
618 views
12
4.4 (15)
I write this hook in nine lines and reuse it everywhere a button needs to jump the page to a specific element. Returning [ref, scroll] mirrors useState's shape so the call site is symmetrical: const [errorRef, scrollToError] = useScrollIntoView(). Defaults of behavior: 'smooth' and block: 'center' are the values I want 95% of the time, and the per-call opts argument lets a caller override them when scrolling a list item to the top of a sticky-header page. The empty deps on useCallback are safe because the ref is a stable container; we read ref.current at call time, not capture time.
This is the hook I forget exists until a QA bug asks why the second article on a navigation chain loads scrolled to the middle. Watching [location.pathname] (or whatever your router exposes) is enough: any route change snaps the container to top, intra-page scroll is left alone. I prefer scrollTo(0, 0) over window.scrollTo because the scroll usually lives on a content container, not the document, in modern layouts with a fixed header and sidebar. The fallback to node.scrollTop = 0 exists because some old test environments provide partial DOM stand-ins; in real browsers you can drop it.
Callback refs are the right tool here because they fire when React attaches AND detaches the node, so unmount cleans up automatically without an effect. The register factory returns a closed-over callback per id; React calls it with the node on mount and with null on unmount, which is the exact lifecycle I want for a Map-backed registry. The mapRef.current === null lazy-init guard avoids paying the new Map() cost on every render, which adds up for components that re-render often. This pattern is what I reach for whenever a docs page, table of contents, or onboarding stepper needs to jump to a child section.
I use this maybe twice a year, but when I need it nothing else fits. forwardRef lets a parent attach a ref to a function component, and useImperativeHandle substitutes what that ref resolves to. The win is that the parent gets exactly { open, close, focus, isOpen } and never the internal dialogRef, so I can refactor the modal's DOM later without breaking call sites. Storing the open flag in a ref keeps the handle's methods correct without depending on a re-render to publish a new closure; in a real component that also wants visual feedback, pair this with a useState mirror that triggers the actual dialog show/hide. Reach for this only when props-down-events-up genuinely cannot express the imperative trigger; otherwise it is just hidden coupling.
