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.
What it is
Section titled “What it is”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.
Why it works this way
Section titled “Why it works this way”Reading the directive prologue is the whole test, and it is deliberately the same test Next.js applies — so the instinct you already have transfers, and there is no second rule to learn for predicting which side a file lands on.
It is also what keeps the lint preset cheap. Deciding a file’s side means reading its
leading statements — no build transform, no import graph, no type information and no
project config. That is why setting up
@homepages/eslint-plugin-template is two lines and never asks for
your tsconfig.json.
How it works
Section titled “How it works”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.
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 contract files (Renderer.tsx, section.ts and 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.
What it is not
Section titled “What it is not”It is not an exemption switch. Marking a file "use client" does not opt it out of
the authoring rules. Exactly three of them stop at this boundary; every other rule runs on
an island exactly as it runs on a server-rendered file.
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.
It is not a promise of interactivity in the editor
Section titled “It is not a promise of interactivity in the editor”The directive decides how a file is linted and how it behaves on the published page. It
says nothing about the editor canvas, where an island stays static SSR HTML unless it
declares live: true — a separate declaration, and a required one. See
require-island-editor.
Where next
Section titled “Where next”- Islands — writing one, its props contract, and its editor options.
- Rule index — every rule id, in both venues.