npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-use-observer-hooks

v1.4.5

Published

Collection of React hooks

Readme

react-use-observer-hooks

npm version

A collection of React hooks for syncing scroll position with navigation state, detecting element visibility, and tracking scroll offset.

Installation

npm install react-use-observer-hooks

Hooks

  • useAnchorObserver — sync page URL with scroll position (single-page anchor navigation)
  • useWasScrolled — detect whether the page (or an element) has been scrolled past an offset
  • useVisible — detect whether a single element is visible in the viewport

useAnchorObserver

useAnchorObserver-demo

Live Demo on StackBlitz

Synchronizes URL/router state with the current scroll position on a single page divided into named sections. When the user scrolls to a section, the URL updates automatically. When the URL changes (e.g., via navigation links or browser back/forward), the page scrolls to the corresponding section.

Signature

useAnchorObserver<T extends HTMLElement>(props: UseAnchorObserverProps): UseAnchorObserver<T>

Props

| Prop | Type | Required | Default | Description | | ---------------- | -------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | anchors | string[] | yes | — | Ordered list of anchor identifiers — one per section, matched by index to the container's children | | currentAnchor | string | yes | — | The currently active anchor (e.g., current URL pathname) | | offsetPx | number | no | 0 | Offset in pixels subtracted from the scroll target position and applied as rootMargin top in IntersectionObserver. Use this when a sticky header overlaps the content. | | throttleMs | number | no | 50 | Throttle interval in milliseconds for IntersectionObserver callbacks | | onAnchorChange | (anchor: string) => void | no | — | Called when the visible section changes due to scrolling |

Return value

| Field | Type | Description | | ---------------- | -------------------------- | -------------------------------------------------------------------------- | | ref | RefCallback<T> | Attach to the container element whose direct children are the sections | | focusedAnchor | string \| undefined | The anchor identifier of the currently visible section | | scrollToAnchor | (anchor: string) => void | Programmatically scroll to a section by its anchor identifier |

How it works

  • ref must be attached to a wrapper element whose direct children correspond 1-to-1 with the anchors array (by index).
  • The hook uses IntersectionObserver to watch all children and determines which one is most visible.
  • When the visible child changes, onAnchorChange is called with the matching anchor string — use this to update the URL.
  • When currentAnchor changes externally (URL change), the hook automatically scrolls to the matching section with a smooth animation.
  • On initial mount, the page instantly scrolls to the section matching currentAnchor (no animation).
  • During programmatic scrolling, observer updates are locked to prevent feedback loops.

Example: Next.js single-page navigation (Pages Router)

This example shows a page with three sections — About, Timeline, Contacts — where each section corresponds to a URL path.

// pages/[[...slug]].tsx
import { useMemo } from 'react';
import { useAnchorObserver } from 'react-use-observer-hooks';
import { usePathname } from 'next/navigation';
import Router from 'next/router';

// Sections and their corresponding URL paths
const SECTIONS = [
  { href: '/', label: 'About', Component: About },
  { href: '/timeline', label: 'Timeline', Component: Timeline },
  { href: '/contacts', label: 'Contacts', Component: Contacts },
];

const ANCHORS = SECTIONS.map(s => s.href); // ['/', '/timeline', '/contacts']

export default function Page() {
  const pathName = usePathname();

  const { ref, focusedAnchor } = useAnchorObserver<HTMLDivElement>({
    anchors: ANCHORS,
    currentAnchor: pathName, // current URL path drives the initial scroll position
    offsetPx: 64, // height of a sticky header, so sections aren't hidden behind it
    onAnchorChange: (anchor: string) => {
      Router.push(anchor, undefined, { scroll: false, shallow: false });
    },
  });

  const currentSection = useMemo(() => SECTIONS.find(s => s.href === focusedAnchor), [focusedAnchor]);

  return (
    <>
      {/* Navigation menu highlights the active section */}
      <Header pages={SECTIONS} currentPage={currentSection} />

      {/*
        The container ref is attached here.
        Each direct child maps to ANCHORS[index]:
          children[0] → '/'
          children[1] → '/timeline'
          children[2] → '/contacts'
      */}
      <div ref={ref}>
        {SECTIONS.map(({ href, Component }) => (
          <section key={href} style={{ minHeight: '100vh' }}>
            <Component />
          </section>
        ))}
      </div>
    </>
  );
}

Navigation link — use scroll={false} so Next.js doesn't scroll on its own; the hook handles scrolling:

<Link href="/timeline" scroll={false} shallow>
  Timeline
</Link>

Programmatic scroll — call scrollToAnchor directly:

const { ref, scrollToAnchor } = useAnchorObserver<HTMLDivElement>({ ... });

<button onClick={() => scrollToAnchor('/contacts')}>
  Go to Contacts
</button>

useWasScrolled

Returns true when the window (or a given element) has been scrolled past offset pixels.

Signature

useWasScrolled(element?: HTMLElement | Window | null, offset?: number): boolean

Parameters

| Param | Type | Default | Description | | --------- | ------------------------------- | -------- | ------------------------------------------------------------- | | element | HTMLElement \| Window \| null | window | Element to observe. Defaults to the browser window. | | offset | number | 0 | Scroll distance in pixels after which the hook returns true |

Example: sticky header shadow

import { useWasScrolled } from 'react-use-observer-hooks';

export const Header = () => {
  const isScrolled = useWasScrolled(); // true once page scrolls past 0px

  return <header className={isScrolled ? 'header header--sticked' : 'header'}>...</header>;
};

With a custom offset — show a "back to top" button only after 300px:

const showBackToTop = useWasScrolled(null, 300);

useVisible

Returns true when a given DOM element intersects the viewport (is visible on screen).

Signature

useVisible(element: Element | null, options?: IntersectionObserverInit & { freezeOnceVisible?: boolean }): boolean

Options

Accepts all standard IntersectionObserver options plus:

| Option | Type | Default | Description | | ------------------- | ----------------- | ------- | ----------------------------------------------- | | threshold | number | 0 | Fraction of the element that must be visible | | root | Element \| null | null | Scroll container (defaults to viewport) | | rootMargin | string | '0%' | Margin around the root | | freezeOnceVisible | boolean | false | Stop observing once the element becomes visible |

Example: lazy-render a heavy component

import { useRef } from 'react';
import { useVisible } from 'react-use-observer-hooks';

const LazySection = () => {
  const ref = useRef<HTMLDivElement>(null);
  const isVisible = useVisible(ref.current, { freezeOnceVisible: true });

  return <div ref={ref}>{isVisible ? <HeavyComponent /> : <Placeholder />}</div>;
};

License

MIT