Skip to content
Twilight React Playground
ثيم رائدaren

RenderWhenVisible

componentBeginnerserverbrowserlive demo

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

Three sections, far apart. Each stays a placeholder until it nears the browser window, then its content mounts and says where it was.Try this: scroll down slowly with rootMargin 0px, press Start again, then repeat with 400px: the sections mount hundreds of pixels before you reach them.
Storefront canvas · en · LTR
Section 1 is not mounted yet. Scroll down.
Section 2 is not mounted yet. Scroll down.
Section 3 is not mounted yet. Scroll down.
Controls
The space held for the section until its content mounts.
renderOnMountMount straight away, with no observer.
What a theme writes
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

app/components/product/ReviewsSection.tsx
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 your className and id. Until that section comes within rootMargin of the viewport it holds the placeholder, and the children are not rendered at all: no effects, no requests.

  • One IntersectionObserver per 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: estimatedHeight until 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 renderOnMount for 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 renderOnMount for 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.

  • rootMargin and renderOnMount matter only before the content appears. Changing them after that does nothing.

Related

Source and docs