SkateSpots/src/components/LazyMount.tsx
SkateSpots 662a14bcd2 perf(marketplace): lazy-mount grid cards + open the detail page on click
- LazyMount: mount a grid card only when it scrolls within 600px (skeleton-space
  placeholder otherwise), so a long feed no longer mounts every card + its
  hooks/DOM up front. Falls back to immediate render without IntersectionObserver.
- GearSwapItemCard: clicking the card now opens /listing/:pubkey/:identifier
  (was opening a DM). 'Contact Seller' still opens the DM. Card set to h-full so
  the lazy wrapper keeps the equal-height grid.

Gate: 101 tests, 0 errors/warnings, boot smoke green.
2026-09-26 23:08:01 +07:00

60 lines
1.5 KiB
TypeScript

import { useEffect, useRef, useState, type ReactNode } from 'react';
/**
* Defer mounting `children` until the wrapper scrolls near the viewport.
*
* Long grids (e.g. the marketplace) otherwise mount every card — and every
* hook/query/DOM node each one pulls in — up front. With this, off-screen cards
* stay as a cheap placeholder until they're ~600px away, then mount once and
* stay mounted. Falls back to immediate render when IntersectionObserver is
* unavailable.
*/
export function LazyMount({
children,
minHeight = 300,
rootMargin = '600px',
className,
}: {
children: ReactNode;
/** Reserve height so the page doesn't jump when the card mounts. */
minHeight?: number;
rootMargin?: string;
className?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
if (visible) return;
const el = ref.current;
if (!el) return;
if (typeof IntersectionObserver === 'undefined') {
setVisible(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setVisible(true);
observer.disconnect();
}
},
{ rootMargin },
);
observer.observe(el);
return () => observer.disconnect();
}, [visible, rootMargin]);
return (
<div
ref={ref}
className={className}
style={visible ? undefined : { minHeight }}
aria-busy={!visible || undefined}
>
{visible ? children : null}
</div>
);
}