Server vs client
The one concept the server-rendering rules assume — how a file is scoped server or client, and which rules stop at that boundary.
Several rules ban something “in server-rendered code” and say an island is exempt. This page is what “server-rendered” and “island” mean, once, so the rule pages do not each re-derive it.
Scoping is per file, by the directive
Section titled “Scoping is per file, by the directive”A file is a client module if and only if its own directive prologue says
"use client". Nothing else decides it — not what it imports, not what imports it,
not the directory it sits in, not a build transform.
The consequence surprises people: the same code fails in one file and passes in the file next to it.
// sections/hero/Renderer.tsx — no directive → server-renderedexport function Renderer() { return <p>{new Date().getFullYear()}</p>; // template-kit/no-nondeterminism}// sections/hero/Clock.tsx — directive → island"use client";
export default function Clock() { return <p>{new Date().getFullYear()}</p>; // fine}Importing an island from a server file does not make the server file an island. It also does not make the island a server file. Each is scoped on its own.
The prologue
Section titled “The prologue”The directive prologue is the run of leading string-literal statements. Comments and blank lines do not end it; any other statement does.
"use strict";"use client"; // still in the prologue → this file IS an islandconst x = 1;"use client"; // real code ended the prologue → this is NOT a directive, and NOT an islandThe second file is the trap. It looks like an island, it is not one, and every server-rendering rule still applies to it. If you meant it to be an island, the directive must be the first statement in the file.
What each side must be
Section titled “What each side must be”A server-rendered file (no directive) runs in the publisher and in the editor, and must be a pure function of its props: same props in, byte-identical HTML out. No clock, no randomness, no effects, no state, no event handlers, no network, no browser globals. It has none of those things available, and the two renders must agree.
An island (with the directive) is server-rendered inline with the rest of the page
and then hydrated in the browser as its own React root. Effects, state, event
handlers, and inline style are all legal there — that is what an island is for. Its
props cross the server→browser boundary as JSON, which is the one new constraint it
takes on (see serializable-island-props).
Interactivity is never banned. It is relocated: out of the server-rendered file, into
a "use client" component in the same section folder, rendered from the Renderer like
any other component.
Exactly three rules exempt islands
Section titled “Exactly three rules exempt islands”| Rule | Exempt on a "use client" file? |
|---|---|
no-nondeterminism |
yes — installs no visitors on an island |
no-client-runtime-in-server |
yes |
no-inline-style |
yes |
| every other rule | no — it runs on island files too |
no-hex deliberately does not exempt islands. The three exemptions above are all
server-render concerns, and an island runs in the browser. A palette is not a render-path
concern: the hydration boundary is not a palette boundary. A section carries no
palette of its own in either render path, so "use client" plus
style={{ color: "#ff0000" }} passes no-inline-style and still fails
no-hex.
The one rule scoped by filename
Section titled “The one rule scoped by filename”no-client-directive-in-contract is governed by
this boundary but is not scoped by the directive — it is scoped by file glob. It
runs on a section’s four contract files (Renderer.tsx, schema.ts, fill-spec.ts,
fixtures.ts) and says those files may never carry the directive at all. They are the
platform’s interface to the section and are read outside a browser; making one an island
would take the section’s server-rendered HTML with it.
See also
Section titled “See also”- Islands — writing one, its props contract, and its editor options.
- Rule index — every rule id, in both venues.