RenderWhenVisible
Shows a placeholder and mounts its children only when the section scrolls near the viewport, then keeps them mounted.
import { RenderWhenVisible } from '@salla.sa/twilight-theme-engine/components/common';In plain words
A long home page can have twenty sections, and many shoppers never scroll to the bottom. RenderWhenVisible waits: it shows a placeholder, and only when the shopper scrolls near the section does it create the real content, with its images and data requests.
In plain JavaScript you would write this with an IntersectionObserver. This component sets one up for you and removes it when it is done.
Signature
const RenderWhenVisible: LazyExoticComponent<(props: RenderWhenVisibleProps) => JSX.Element>
// RenderWhenVisibleProps is not exported
interface RenderWhenVisibleProps {
children: ReactNode;
rootMargin?: string; // default '50px': start this far before the viewport
placeholder?: ReactNode; // default: a grey pulsing box
estimatedHeight?: string; // default '400px': space held until the content mounts
renderOnMount?: boolean; // default false: true mounts at once, no observer
className?: string; // on the <section> it renders
id?: string;
}Try it live
import { RenderWhenVisible } from '@salla.sa/twilight-theme-engine/components/common';
import { Reviews } from './Reviews';
export function ProductReviews({ productId }: { productId: number }) {
return (
<RenderWhenVisible estimatedHeight="120px">
<Reviews productId={productId} />
</RenderWhenVisible>
);
}
Example
import { RenderWhenVisible } from '@salla.sa/twilight-theme-engine/components/common';
import { SkeletonPulse } from '@salla.sa/twilight-theme-engine/skeleton';
import { ProductReviews } from './ProductReviews';
export function ReviewsSection({ productId }: { productId: number }) {
return (
<RenderWhenVisible
estimatedHeight="320px"
placeholder={<SkeletonPulse style={{ height: 320 }} />}
>
<ProductReviews productId={productId} />
</RenderWhenVisible>
);
}
How it behaves
It renders a
<section>with yourclassNameandid. Until that section comes withinrootMarginof the viewport it holds the placeholder, and the children are not rendered at all: no effects, no requests.One
IntersectionObserverper section, disconnected at the first intersecting entry. From then on the children stay mounted, even when scrolled out of view.The children render inside
<Suspense fallback={placeholder}>, so a lazy component or suspending data inside shows the same placeholder.The section keeps
min-height: estimatedHeightuntil the children have rendered, then drops it and lets the content decide the height.The engine's home page wraps each block in it, with
renderOnMountfor the first one.It is lazy (code-split).
Gotchas
The server renders only the placeholder, because the observer runs in the browser. Content inside is missing from the HTML search engines read and from the first paint. Use
renderOnMountfor anything important for SEO or above the fold.estimatedHeight(400px by default) applies even when your placeholder is shorter, and the space shrinks when a shorter block mounts, so the page below jumps. Set it close to the real height.rootMarginandrenderOnMountmatter only before the content appears. Changing them after that does nothing.