Skip to content
DocsReferenceHooks

Hooks

HookPackagePurpose
useMasonry@repo/masonry-engineMemoized masonry with incremental append
useMasonryWorker@repo/masonry-engineOff-main-thread masonry (Web Worker)
useJustifiedLayout@repo/justified-layoutMemoized justified layout
useFixedGrid@repo/justified-layoutMemoized fixed-aspect grid
useContainerSize@repo/uiResizeObserver width/height (rAF-coalesced)
useVirtualGrid@repo/uiWindowing over positioned items
useInfiniteImages@repo/uiIntersectionObserver-driven background paging, bi-directional (append + prepend)
useScrollAnchor@repo/uiKeeps the viewport steady when content is prepended above

Putting it together

A complete masonry feed is four hooks and a renderer:

const { ref, width } = useContainerSize<HTMLDivElement>();
 
const { images, sentinelRef } = useInfiniteImages({
  initial,
  fetchPage: (page, size) => fetchImagePage(page, size, seed),
});
 
const { positions, containerHeight } = useMasonry(images, { containerWidth: width });
 
return (
  <div ref={ref}>
    <VirtualCanvas
      positions={positions}
      containerHeight={containerHeight}
      overscan={500}
      sentinel={<div ref={sentinelRef} />}
      renderItem={(item, priority) => (
        <ImageTile item={item} priority={priority} sizes={`${item.width}px`} />
      )}
    />
  </div>
);

Bi-directional: scrolling back up

Passing fetchPreviousPage makes the stream two-ended. Scrolling toward the top requests earlier pages; useScrollAnchor cancels the coordinate shift so the view never jumps:

const { images, sentinelRef, loadEarlier, hasEarlier, earlierError, prependCount } =
  useInfiniteImages({
    initial,
    fetchPage: (page, size) => fetchImagePage(page, size, seed),
    fetchPreviousPage: (page, size) => fetchImagePage(page, size, seed), // pages go -1, -2, …
  });
 
const { capture } = useScrollAnchor({ positions, prependCount });
 
// capture() must run while positions still describe the old layout.
useEffect(() => {
  const onScroll = () => {
    const event = window.scrollY;
    const delta = event - prevScrollRef.current;
    prevScrollRef.current = event;
    if (event < 1200 && delta < 0 && hasEarlier && !earlierError) {
      capture();
      loadEarlier();
    }
  };
  window.addEventListener("scroll", onScroll, { passive: true });
  return () => window.removeEventListener("scroll", onScroll);
}, [hasEarlier, earlierError, capture, loadEarlier]);

The restore runs in a useLayoutEffect — before paint — so the browser never displays the displaced frame. The upward-only delta < 0 guard prevents a spurious prepend on the first downward pass past the threshold.

useInfiniteImages

const {
  images, loading, error, hasMore,
  loadMore, retry, sentinelRef,
  // bi-directional (present only when fetchPreviousPage is provided):
  hasEarlier, loadEarlier, earlierError, earlierRetry, prependCount,
} = useInfiniteImages({
  initial,            // server-rendered first page, kept as the stable prefix
  fetchPage,          // (page: number, pageSize: number) => Promise<ImageData[]>
  fetchPreviousPage,  // optional — enables prepending (pages count down from -1)
  pageSize = 1000,    // optional
  maxImages = 5000,   // optional hard cap
});

loadEarlier prepends the previous page above the current stream; the hook preserves the identity of every existing image object, so the masonry engine appends in O(delta) and already-laid tiles keep their coordinates. Feed prependCount into useScrollAnchor to keep the viewport fixed. A failed earlier fetch surfaces earlierError and pauses further prepends until earlierRetry() — the same pause/resume contract as the forward error/retry.

useScrollAnchor

const { capture } = useScrollAnchor({
  positions,           // current layout positions
  prependCount,        // useInfiniteImages.prependCount
  scrollContainerRef,  // optional; defaults to window
});

Call capture() immediately before loadEarlier(), while positions still describe the pre-prepend layout. When prependCount advances (the prepend has re-laid out), the hook restores the scroll offset in a layout effect, so the same tile ends up at the same viewport position. The restore is an instant, non-smooth window.scrollTo (or container scrollTop), so it reads the same under prefers-reduced-motion — no animated jump is ever produced.

useContainerSize

const { ref, width, height } = useContainerSize<HTMLDivElement>();

Measures an element with a ResizeObserver, coalescing bursts to one update per frame. Layout recomputes downstream the instant the width crosses a breakpoint.

useVirtualGrid

const { containerRef, visible } = useVirtualGrid({
  positions,
  containerHeight,
  overscan: 500,
  adaptiveOverscan: true,
  onSample: (s) => { sampleRef.current = s; }, // visibleCount, velocity, overscan
});

Returns only the items in view (+ overscan). The onSample callback writes to a ref (never triggers a render) and feeds the benchmark HUD.