🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Passing props to islands

Pass props to a component marked mochi:hydrate, mochi:hydrate:visible, or mochi:defer as you would to any Svelte component. Mochi serializes them with devalue so the same values reach the hydrating client.

<!-- file: src/routes/Page.svelte -->
<script>
  const user = { name: 'Ada', id: 42 };
  const visitedAt = new Date();
  const tags = new Set(['svelte', 'bun']);
</script>

<UserCard mochi:hydrate {user} {visitedAt} {tags} />

Typing props

Put the type on the let { … } = $props() declaration. For a few props, inline the type:

<script lang="ts">
  let { adjective }: { adjective: string } = $props();
</script>

For larger or reused shapes, use a Props interface:

<script lang="ts">
  interface Props {
    title: string;
    count?: number;
    user: { name: string; id: number };
  }

  let { title, count = 0, user }: Props = $props();
</script>

Type snippet props (including children) with the Snippet interface from svelte. Snippets can only be passed between components on the same side of the server→client boundary — inside an island’s subtree, or between server-rendered components — never into a hydrate island from the page (see below):

<script lang="ts">
  import type { Snippet } from 'svelte';

  let { children }: { children: Snippet } = $props();
</script>

{@render children()}

When a component wraps a native element and forwards its attributes, type the spread with the matching interface from svelte/elements:

<script lang="ts">
  import type { HTMLButtonAttributes } from 'svelte/elements';

  let { children, ...rest }: HTMLButtonAttributes = $props();
</script>

<button {...rest}>{@render children?.()}</button>

How props travel

For mochi:hydrate* islands, props ship inline in the page HTML. When several islands share the same payload, it ships over the wire once and the rest reference it, which keeps the page small. For mochi:defer server islands, props are encrypted (opaque on the wire) and passed to a per-island endpoint. See Server islands.

Supported types

  • Plain objects and arrays
  • Primitives: strings, numbers, booleans, null
  • Date, RegExp, Map, Set, URL, URLSearchParams
  • BigInt, typed arrays (Uint8Array, and so on)
  • undefined, Infinity, NaN, -0
  • Repeated and cyclic references (identity is preserved)

Unsupported types

  • Functions
  • Class instances (only own enumerable properties survive)
  • Symbol
  • Snippets / children — a snippet is a function, so it cannot cross the boundary. Children at a mochi:hydrate* call site are a compile error; on mochi:defer* and mochi:clientOnly* they are the loading fallback instead.

Detecting hydration

To branch on whether the current render will hydrate, call isHydratable(). It works in any component at any nesting depth, with no prop involved.

<!-- file: src/lib/UserCard.svelte -->
<script lang="ts">
  import { isHydratable } from 'mochi-framework';

  let { user }: { user: { name: string; id: number } } = $props();

  const hydratable = isHydratable();
</script>

islandId is a reserved name on every island. Passing it as a literal prop is a compile error, so a component can move between directives without the name changing meaning. For a unique id inside the component, use $props.id().

See it in action

Live demos showing key concepts from this page