Skip to main content

ClientOnly

Overview​

ClientOnly renders its children only in the browser, after hydration, and a fallback until then, so content that depends on browser-only state doesn't cause a hydration mismatch.

The server render and the hydrating client render both show fallback, which keeps them identical; the children replace it in the commit that follows. Reach for it when the markup itself would differ between server and browser: the reader's time zone or locale, a value from localStorage, the viewport size, a widget that needs window to render at all. In an app without server rendering the children show on the first render, so wrapping something in ClientOnly costs nothing there.

The hook underneath, useIsHydrated, is exported too, for components that need the answer rather than a wrapper.


Import​

import { ClientOnly, useIsHydrated } from '@allxsmith/bestax-bulma';

Hooks​

useIsHydrated​

Returns false during server rendering and during the hydrating client render, then true from the commit that follows. A component that mounts after hydration (after a route change, or in an app with no server rendering) gets true on its first render, so it pays for no extra render.

function Greeting() {
const hydrated = useIsHydrated();
// localStorage only exists in the browser, so the server render and the
// hydrating render both show the generic greeting.
const name = hydrated ? localStorage.getItem('name') : null;
return <Title size="4">{name ? `Welcome back, ${name}` : 'Welcome'}</Title>;
}

Usage​

Browser-only content​

Pass the content as a function and it is called only after hydration, so an expression that reads window, document or Intl state never runs on the server. Plain children work too, but JSX children are evaluated by the component that renders ClientOnly, on the server included; a function is what keeps a browser-only expression off the server.

<ClientOnly fallback={<Skeleton variant="lines" lines={1} />}>
  {() => (
    <Notification color="info">
      Times are shown in{' '}
      <strong>{Intl.DateTimeFormat().resolvedOptions().timeZone}</strong>.
    </Notification>
  )}
</ClientOnly>

A component that needs the browser​

A component's own render is deferred along with it, so a component child works without the function form.

<ClientOnly fallback={<Skeleton variant="block" />}>
<MapWidget center={office} />
</ClientOnly>

A fallback that holds the layout​

Without a fallback nothing renders until hydration, and the content pushes the page down when it arrives. A placeholder of the same size, such as a Skeleton, avoids the jump.

<ClientOnly fallback={<Skeleton variant="lines" lines={3} />}>
{() => <RecentlyViewed items={readHistory(localStorage)} />}
</ClientOnly>

Accessibility​

  • Content that arrives after hydration is not announced by screen readers on its own. Keep what a reader needs on first load out of ClientOnly, and use it for enhancements.
  • Match the fallback's size to the content, so the page doesn't shift under someone who has started reading or scrolling.
  • Assistive technology meets the fallback until it is replaced. An empty Skeleton gives it nothing to read; a text fallback such as "Loading…" is read out.

  • Modal: Its portal prop follows the same rule, rendering inline until the page has hydrated.
  • Skeleton: A placeholder to pass as fallback.
  • Reveal: Renders its final state on the server and animates only in the browser.

Additional Resources​


Props​

PropTypeDefaultDescription
childrenReactNode | () => ReactNode—Content to render once hydrated. A function is called only then, never on the server.
fallbackReactNodenullRendered on the server and during hydration instead, such as a placeholder the same size as the content.