Review
Auto-page vinyl crate dig with sentinel observer
CrateDig buyers were hammering Load more while flipping new pressings in the marketplace crate. Replace the button with a bottom sentinel watched by IntersectionObserver, use rootMargin so the next page prefetches before the footer enters view, and disconnect on cleanup.
ReactTier 1reactintersection-observerinfinite-scroll
Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.
components/market/CrateBrowser.tsx+16-5
| 18 | 18 | export function CrateBrowser({ pressings, hasMore, loading, onLoadMore }: Props) { | |
| 19 | + | const sentinelRef = useRef<HTMLDivElement>(null); | |
| 19 | 20 | ||
| 21 | + | useEffect(() => { | |
| 22 | + | const el = sentinelRef.current; | |
| 23 | + | if (!el || !hasMore) return; | |
| 24 | + | const io = new IntersectionObserver( | |
| 25 | + | ([entry]) => { | |
| 26 | + | if (entry.isIntersecting && !loading) onLoadMore(); | |
| 27 | + | }, | |
| 28 | + | // Prefetch the next crate page ~240px before the sentinel hits the viewport | |
| 29 | + | { root: null, rootMargin: "240px 0px", threshold: 0 }, | |
| 30 | + | ); | |
| 31 | + | io.observe(el); | |
| 32 | + | return () => io.disconnect(); | |
| 33 | + | }, [hasMore, loading, onLoadMore]); | |
| 20 | 34 | ||
| 21 | 35 | return ( | |
| 22 | 36 | <div className="crate-grid"> | |
| 23 | 37 | {pressings.map((p) => ( | |
| 24 | 38 | <PressingCard key={p.id} pressing={p} /> | |
| 25 | 39 | ))} | |
| 26 | - | {hasMore && ( | |
| 27 | - | <button type="button" disabled={loading} onClick={onLoadMore}> | |
| 28 | - | {loading ? "Spinning…" : "Load more"} | |
| 29 | - | </button> | |
| 30 | - | )} | |
| 40 | + | {hasMore && <div ref={sentinelRef} className="crate-sentinel" aria-hidden />} | |
| 41 | + | {loading && <p className="crate-status">Spinning the next crate…</p>} | |
| 31 | 42 | </div> | |
| 32 | 43 | ); | |
| 33 | 44 | } |