LangENKO

Client Rendering

`"use client"` pages, `*.island.tsx` islands, and partials — when to pick which.

since v0.22
On this page

Client Rendering

Mandu has three ways to run React on the client. They look similar but serve different purposes — picking the right one keeps pages fast.

The three mechanisms

Mechanism Scope Initial HTML Bundle size
"use client" page Whole route None (CSR shell) Large
Island (*.island.tsx) One component in a server page Full page SSR/prerender, island placeholder Per-island chunk
Partial One element (button, field) Server-rendered then hydrates Smallest

"use client" page

A page file starting with "use client" runs entirely on the client. Mandu ships a client bundle and the browser boots React to render the whole route.

// app/editor/page.tsx
"use client";
import { useState } from "react";

export default function Editor() {
  const [text, setText] = useState("");
  return <textarea value={text} onChange={(e) => setText(e.target.value)} />;
}

Use for fully interactive routes: editors, dashboards, games, drawing apps. Loses prerender + SEO — don't use for marketing or docs pages.

Island (*.island.tsx)

An island is a component that hydrates on the client inside an otherwise server-rendered page. Place the file next to page.tsx:

app/article/[id]/
├── page.tsx              # server component (SSR or prerender)
└── reactions.island.tsx  # island (client-hydrated)
// app/article/[id]/reactions.island.tsx
"use client";
import { useState } from "react";

export default function Reactions({ articleId }: { articleId: string }) {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(l => l + 1)}>❤️ {likes}</button>;
}
// app/article/[id]/page.tsx (server component)
import Reactions from "./reactions.island";

export default async function Article({ params }) {
  const article = await db.articles.findOne({ id: params.id });
  return (
    <main>
      <h1>{article.title}</h1>
      <article dangerouslySetInnerHTML={{ __html: article.html }} />
      <Reactions articleId={article.id} />
    </main>
  );
}

The page renders on the server (HTML streams to browser), then Mandu loads the island's JS bundle and hydrates just that button. Everything else stays static.

Hydration strategies

Islands support four hydration priorities:

  • immediate — hydrate as soon as bundle loads (default for above-the-fold)
  • visible — wait for IntersectionObserver (below-the-fold)
  • idle — use requestIdleCallback
  • interaction — wait for user hover/click/focus

Set via frontmatter or the priority option when calling island().

Partial

A partial is smaller than an island — you mark a single component (a button, a form field) as a hydration unit. Server renders it for initial HTML, then Mandu hydrates only that node on demand.

// spec/partials/like-button.partial.ts
import { partial } from "@mandujs/core/client";
import { LikeButton } from "@/client/shared/ui/like-button";

export const LikeButtonPartial = partial({
  component: LikeButton,
  priority: "interaction",
});
// Used in a server page
<LikeButtonPartial.Render postId="abc" />

Partials are right when you have one interactive element inside a server-rendered cluster (e.g. a delete button in a table row). For multiple siblings that share state, use an island instead.

Decision tree

Is the page fully interactive (editor, dashboard)?
 └── YES → "use client" page

Is only part of the page interactive?
 └── YES → Island (.island.tsx)

Is it just ONE element (a button, a field)?
 └── YES → Partial

What NOT to do

  • Do not add "use client" to app/page.tsx expecting an island — it turns the entire route into CSR. Create a sibling *.island.tsx instead.
  • Do not import server-only modules (node:fs, DB clients) from an island. Guard blocks this at build time.
  • Do not hand-write <script> hydration glue. Mandu's bundler emits it.

🤖 Agent Prompt

🤖 Agent Prompt — Client Rendering
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/client-rendering to my project.

Summary of the page:
Mandu has three client-render mechanisms: 'use client' page (whole page client-rendered), .island.tsx (partial hydration of a component inside a server page), and partial (even smaller than island, for single buttons). Prefer islands for most interactive UI.

Required invariants — must hold after your changes:
- `*.island.tsx` files must live next to a `page.tsx` and include `\"use client\"`
- Page-level `\"use client\"` opts the whole route out of prerender
- Partials render server-side for initial HTML then hydrate on demand

Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
   in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.

For Agents

AI hint

Mandu has three client-render mechanisms: 'use client' page (whole page client-rendered), .island.tsx (partial hydration of a component inside a server page), and partial (even smaller than island, for single buttons). Prefer islands for most interactive UI.

Invariants
  • `*.island.tsx` files must live next to a `page.tsx` and include `\"use client\"`
  • Page-level `\"use client\"` opts the whole route out of prerender
  • Partials render server-side for initial HTML then hydrate on demand
Guard scope
architecture