Skip to content

ClientOnly

olegivanov edited this page May 18, 2026 · 1 revision

ClientOnly

1. Overview

  • Name: ClientOnly
  • Purpose: Render a fallback (default: null) on the server, swap to children after hydration on the client.
  • When to use: Wrap UI that depends on browser-only APIs (window, localStorage, IntersectionObserver, geolocation, etc.) and would crash or differ between server and client.
  • Availability: All 5 framework adapters — @real-router/react/ssr, @real-router/preact/ssr, @real-router/solid/ssr, @real-router/vue/ssr, @real-router/svelte/ssr. Semantics identical across adapters.

2. Import and Basic Usage

<script lang="ts">
  import { ClientOnly } from "@real-router/svelte/ssr";

  import BrowserApiWidget from "./BrowserApiWidget.svelte";
  import Skeleton from "./Skeleton.svelte";
</script>

<ClientOnly>
  {#snippet children()}
    <BrowserApiWidget />
  {/snippet}
  {#snippet fallback()}
    <Skeleton />
  {/snippet}
</ClientOnly>

On the server, only the Skeleton is emitted to HTML. Once the component hydrates on the client, an $effect flips an internal mounted flag and BrowserApiWidget replaces the skeleton.

3. Props

Prop Type Required Default Description
children Snippet Yes Snippet rendered only on the client (after hydration).
fallback Snippet No undefined Snippet rendered on the server and immediately after hydration before the $effect fires. Omit to emit nothing on the server.

4. Behavior

Implementation

<script lang="ts">
  let mounted = $state(false);

  $effect(() => {
    mounted = true;
  });
</script>

{#if mounted}
  {@render children()}
{:else if fallback}
  {@render fallback()}
{/if}
  • $effect does NOT run on the server (Svelte's SSR compiler emits the rune as a no-op).
  • Initial render produces mounted = false → fallback (or nothing) ships.
  • Client mounts the component, $effect fires, mounted = true → re-renders with children.

Hydration mismatches

ClientOnly is hydration-safe in Svelte 5: the framework tolerates the {#if mounted} branch flipping between server (false) and client (true after effect). No console.error warnings, no DOM-mismatch crashes — verified by examples/web/svelte/ssr-examples/ssr/.

This contrasts with Vue/Solid which require symmetric provider mounting; Svelte's hydration walker accepts asymmetric branches.

No SEO indexing

Content rendered inside <ClientOnly> does NOT appear in the SSR HTML. Search engines and link previewers see only the fallback. Move SEO-critical content out of <ClientOnly>.

5. Examples

Browser-only widget (no SEO impact)

<script lang="ts">
  import { ClientOnly } from "@real-router/svelte/ssr";

  import Map from "./Map.svelte"; // uses window.navigator.geolocation
</script>

<ClientOnly>
  {#snippet children()}
    <Map />
  {/snippet}
  {#snippet fallback()}
    <div class="map-skeleton" aria-label="Loading map">Loading map…</div>
  {/snippet}
</ClientOnly>

Silent (no fallback) — useful when an empty server render is OK

<ClientOnly>
  {#snippet children()}
    <PortalMount />
  {/snippet}
</ClientOnly>

localStorage-dependent UI

<script lang="ts">
  import { ClientOnly } from "@real-router/svelte/ssr";
</script>

<ClientOnly>
  {#snippet children()}
    {@const theme = localStorage.getItem("theme") ?? "light"}
    <ThemeToggle initial={theme} />
  {/snippet}
  {#snippet fallback()}
    <ThemeToggle initial="light" />
  {/snippet}
</ClientOnly>

Pair with <ServerOnly> for symmetric splits

<ServerOnly>
  {#snippet children()}
    <SeoMetaStrip />
  {/snippet}
</ServerOnly>

<ClientOnly>
  {#snippet children()}
    <InteractiveDashboard />
  {/snippet}
</ClientOnly>

6. Gotchas

  • No SSR content for SEO. Content inside children only exists post-hydration. If the content matters for SEO/social previews, either move it out or duplicate critical info in the fallback.
  • Flash on hydration. Users see fallback briefly during hydration. Use a skeleton that matches the final layout (same dimensions) to avoid CLS (Cumulative Layout Shift).
  • $effect is the gate. The flip happens after the initial post-hydration tick. For "render-time" client detection, this is fine — but don't rely on mounted for synchronous logic in <script>.
  • No prop to opt out of fallback on first paint. The fallback always renders on server + before-mount on client. If you need an empty render on the server but a synchronous detection on the client, use a typeof window !== "undefined" check inside a regular component (sacrificing SSR/CSR template parity).

7. Dependencies

  • No <RouterProvider> requirement — <ClientOnly> is environment-only, not router-aware.
  • Requires Svelte 5.x for runes.

8. Related

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

SSR-feature subpath — @real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.

  • Lazy (Svelte only — dynamic component import)
  • Await — read deferred SSR data by key
  • Streamed — Suspense-style boundary
  • ClientOnly — server fallback → client children swap
  • ServerOnly — server children, removed after hydration
  • HttpStatusCode — render-time HTTP status declaration
  • HttpStatusProvider — provides sink to descendant <HttpStatusCode>
  • useDeferred — read deferred Promise by key

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally