Client Rendering
`"use client"` pages, `*.island.tsx` islands, and partials — when to pick which.
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— userequestIdleCallbackinteraction— 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"toapp/page.tsxexpecting an island — it turns the entire route into CSR. Create a sibling*.island.tsxinstead. - 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
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.
Related
- Rendering modes — all 8 modes
- SSR — server-rendered parents of islands
- Island — deep dive on island API
For Agents
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.
- `*.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