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.

JavaScript
Frontend
4 snippets
react
hooks
references
js-dom
rajtanaka

By @rajtanaka

November 26, 2025

·

Updated May 18, 2026

618 views

12

4.4 (15)

// Recipe 1. The simplest one. Returns [ref, scroll]. Caller attaches ref to
// any DOM node, then calls scroll(opts) to bring it into view.
const { useRef, useCallback } = (typeof React !== 'undefined' ? React : {
    useRef: (init) => ({ current: init }),
    useCallback: (f) => f,
});

function useScrollIntoView(defaultOpts) {
    const ref = useRef(null);
    const scroll = useCallback((opts) => {
        const node = ref.current;
        if (!node) return false;
        const merged = Object.assign({ behavior: 'smooth', block: 'center' }, defaultOpts || {}, opts || {});
        if (typeof node.scrollIntoView === 'function') node.scrollIntoView(merged);
        else console.log('would call scrollIntoView with', merged);
        return true;
    }, []);
    return [ref, scroll];
}

// Stand-in for a real DOM node so the snippet runs anywhere.
const calls = [];
const fakeNode = { scrollIntoView: (opts) => { calls.push(opts); } };

const [ref, scroll] = useScrollIntoView();
console.log('scroll before attach ->', scroll());
ref.current = fakeNode;
scroll();
scroll({ block: 'start' });
scroll({ behavior: 'auto' });
console.log('captured calls:', JSON.stringify(calls));

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.