ReactThe same page split badly and well, with what ships in each case
import { useState } from "react";
const BAD = `// app/article/page.jsx
"use client"; // the whole page is now a client module
import { marked } from "marked"; // 40KB -> shipped to the browser
import { formatDistance } from "date-fns"; // 20KB -> shipped
import { useState } from "react";
export default function ArticlePage({ article }) {
const [open, setOpen] = useState(false); // <- the ONLY reason for the directive
return (
<article>
<h1>{article.title}</h1>
<time>{formatDistance(article.date, new Date())}</time>
<div dangerouslySetInnerHTML={{ __html: marked(article.body) }} />
<button onClick={() => setOpen(!open)}>Comments</button>
{open && <Comments articleId={article.id} />}
</article>
);
}`;
const GOOD = `// app/article/page.jsx — NO directive: a Server Component
import { marked } from "marked"; // stays on the server
import { formatDistance } from "date-fns"; // stays on the server
import Disclosure from "./Disclosure"; // the client leaf
export default async function ArticlePage({ params }) {
const article = await db.article.find(params.id); // no API route needed
return (
<article>
<h1>{article.title}</h1>
<time>{formatDistance(article.date, new Date())}</time>
<div dangerouslySetInnerHTML={{ __html: marked(article.body) }} />
{/* Disclosure is a client component, but Comments is passed as
CHILDREN — already rendered on the server, so it does NOT ship. */}
<Disclosure label="Comments">
<Comments articleId={article.id} />
</Disclosure>
</article>
);
}
// app/article/Disclosure.jsx
"use client"; // the directive lives HERE, on the leaf
import { useState } from "react";
export default function Disclosure({ label, children }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(!open)}>{label}</button>
{open && children}
</>
);
}`;
const SHIPS = [
["marked (markdown renderer)", "~40KB", false, true],
["date-fns (formatting)", "~20KB", false, true],
["the page component itself", "~2KB", false, true],
["the Disclosure toggle", "~0.4KB", true, true],
];
export default function App() {
const [good, setGood] = useState(true);
const total = SHIPS.filter(([, , inGood, inBad]) => (good ? inGood : inBad))
.reduce((n, [, size]) => n + parseFloat(size.replace(/[^\d.]/g, "")), 0);
return (
<div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 640 }}>
<div style={{ display: "flex", gap: 8, marginBottom: 10 }}>
<button onClick={() => setGood(false)} style={{ fontWeight: !good ? "bold" : "normal" }}>
❌ directive on the page
</button>
<button onClick={() => setGood(true)} style={{ fontWeight: good ? "bold" : "normal" }}>
✅ directive on the leaf
</button>
</div>
<pre style={{ background: good ? "#f2f9f2" : "#fdf0f0", border: "1px solid #ddd",
borderRadius: 8, padding: 12, fontSize: 12, overflowX: "auto", whiteSpace: "pre-wrap" }}>
{good ? GOOD : BAD}
</pre>
<h4 style={{ margin: "12px 0 6px" }}>What reaches the browser</h4>
<table style={{ width: "100%", fontSize: 13, borderCollapse: "collapse" }}>
<tbody>
{SHIPS.map(([name, size, inGood, inBad]) => {
const ships = good ? inGood : inBad;
return (
<tr key={name} style={{ borderBottom: "1px solid #eee", opacity: ships ? 1 : 0.4 }}>
<td style={{ padding: "3px 8px 3px 0" }}>{name}</td>
<td style={{ color: "#666" }}>{size}</td>
<td style={{ color: ships ? "#a33" : "#161" }}>{ships ? "ships" : "stays on the server"}</td>
</tr>
);
})}
</tbody>
</table>
<p style={{ fontSize: 13, marginTop: 6 }}>
client bundle: <strong>{total.toFixed(1)}KB</strong>
</p>
<p style={{ fontSize: 13, color: "#666" }}>
The behaviour is identical in both. One <code>useState</code> was the
only thing forcing the directive, and moving it to a leaf that takes{" "}
<code>children</code> keeps the markdown renderer, the date library and
the page itself entirely on the server.
</p>
</div>
);
}