Skip to content

PATTERN Cited by 1 source

Mount-gated client-only rendering

Pattern. For content that should genuinely differ between server-side and client-side rendering (device-specific banners, viewport-dependent UI, anything that uses navigator.* or window.*), render a fallback (or null) on both SSR and initial CSR, then flip to the real content after the component mounts on the client.

The flip is driven by a standard React lifecycle shape:

const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
  setIsMounted(true);
}, []);

return (
  <div>
    {isMounted ? dataThatDiffersBetweenClientAndServer : "some fallback" || null}
  </div>
);

(Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react.)

On SSR, isMounted is false → fallback. On initial CSR hydration, same value → fallback. Markup matches. No mismatch. After hydration completes, the useEffect runs, setIsMounted(true), component re-renders with the real content.

Shape of mismatch this solves

Instead of (SSR renders one thing, CSR renders different thing → mismatch):

const { dataThatDiffersBetweenClientAndServer } = props;
return <div>{dataThatDiffersBetweenClientAndServer}</div>;

Do:

const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => { setIsMounted(true); }, []);
return (
  <div>{isMounted ? dataThatDiffersBetweenClientAndServer : "some fallback" || null}</div>
);

When this pattern is correct

  • Device / browser / OS detection that relies on client-only APIs (navigator.userAgent, window.matchMedia, etc).
  • App-download banners tailored to the user's device.
  • Personalisation / preferences only readable from localStorage or cookies the server doesn't see.
  • Component variants whose choice depends on client feature detection (WebGL available? WebCrypto subtle?).

When NOT to reach for this pattern

  • Timezone / locale mismatchespatterns/backend-localization-for-hydration-stability is a better architectural fix; mount-gating these just delays the render and breaks SEO (the server-rendered content doesn't carry the localised value).
  • Timer / time-delta contentpatterns/suppress-hydration-warning-for-unavoidable-mismatch preserves the initial SSR value without a post-mount flip.
  • Anything that should be in the initial SEO-indexed content — mount-gating hides the real content from Googlebot's stateless render unless the fallback carries the same semantic content.
  • Above-the-fold critical UI — the fallback → real-content flip is visible; if it happens above the fold the user sees a flash of the fallback followed by the real content.

Layout-shift risk

The post calls this out explicitly: "in such cases, be mindful of layout shifts that can happen as a result of some element popping into the view." (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react.)

Mitigations:

  • Size the fallback to match the real content's bounding box — e.g. a height/min-height-set placeholder.
  • Use skeleton UI that occupies the same region with a neutral-colour pattern.
  • Position absolutely if the gate is decorating existing content rather than replacing it.
  • Consider hiding the region with CSS visibility: hidden until mount-gated, if the content's bounding box is SSR-knowable.

Trade-offs vs alternatives

Approach SSR output CSR flip Preserves SEO Hydration-safe
Mount-gated rendering fallback yes only if fallback is semantic
suppressHydrationWarning server value implicit yes ✅ (narrow)
Server-value-matching-client (via explicit args) correct no yes
Backend-computed value correct no yes

Read: if you can make server and client agree (by passing explicit timezone / locale / whatever), that's always better than mount-gating — no flip, no layout shift, no SEO cost. Mount-gating is the escape hatch for genuinely client-only data.

Interaction with concurrent rendering

Under React 18 / concurrent rendering + streaming SSR, the mount-gated pattern still works as described but the timing of the flip is tied to when the boundary that contains it is hydrated, not when the whole page finishes hydrating. This means the flip can happen before the rest of the page is interactive — slightly earlier than React 17, but with the same semantics.

Seen in

Last updated · 501 distilled / 1,218 read