Advanced · for app developers · Markdown

Islands and hydration

Server-rendered components

Most components never reach the browser. The build lowers them and the host renders them, so they only cost markup. A component qualifies when it is a function of its props: it may branch, map over arrays, compose other lowered components and read what it was given.

It may not hold state, reach for a browser API or call a hook. Any of those makes a component an island.

Declaring an island

tsx
import { Island } from "@snapfire/fsr-client/react";
import { Cart } from "@src/ui/Cart";

<Island when="visible">
  <Cart items={items} />
</Island>;

when decides when it hydrates:

whenHydrates
loadAs soon as the entry module runs
visibleWhen it scrolls into view
idleWhen the browser is idle

A page and a layout are each an island in their own right. They hydrate in separate roots, which is why a navigation can swap the page and leave the layout's DOM and state untouched.

Island keys and state

Every placement carries a key the build gave it and the server writes the same key on the markup it rendered there. The browser pairs the two by that key rather than by the order they appear in.

Because of this an island is safe inside a .map or behind a condition. If you reorder the list, drop an entry or flip the condition, each island still finds the markup that belongs to it. An open menu or a half-typed field stays with its own row instead of moving to its neighbour.

Server islands

mode="server" is the other kind. The component's interactions round-trip to Rust, which answers with a patch. The browser ships no component code at all.

tsx
<Island when="visible" mode="server">
  <TelemetryBadge />
</Island>

Use it for a disclosure whose content is expensive or for anything whose state belongs on the server. Every interaction is a request, so do not use it for anything that has to feel instant, such as a hover, a drag or a text field.

Islands in something other than React

An island's registry entry names a mounter. Nothing above the entry depends on which one. A .vue component, a custom element definition or an htmx region can each take the place of a React island. More than one kind can sit in the same document. See Vue, Web Components, htmx and Tera.

What an island costs

The islands and the client runtime are the only JavaScript in the bundle, plus whatever app/vendor/ holds. The import map is the allowlist that limits it: an import with no entry fails the build.

Vendoring is explicit and committed:

text
fsr add app lodash-es@4.17.21

That writes an ESM bundle under app/vendor/ and adds the import-map entry. The tree is committed, so a clone builds offline and nothing is fetched at deploy time.

Try it

Take a component that uses useState for something the server already knows and delete the state. Watch it drop out of the bundle and into the rendered list fsr build --verbose prints. That list shows how much of your application the browser has to run.