Islands
Islands enable client side interactivity in Fresh. An island is an isolated Preact component that is rendered on the server like any component, and then hydrated on the client so it can respond to user input. Everything outside an island stays static HTML. No JavaScript is shipped for it.
Islands are defined by creating a file in the islands/ folder in a Fresh project. The exported component is a normal Preact component.
import { useSignal } from "@preact/signals";
export function Counter() {
const count = useSignal(0);
return (
<button type="button" onClick={() => count.value++}>
Count: {count}
</button>
);
}An island can be used in a page like a regular Preact component. Fresh detects that it is an island and takes care of hydrating it automatically.
import { page } from "./$index.ts";
import { Counter } from "../islands/Counter.tsx";
export default page(() => (
<main>
<h1>Welcome</h1>
<Counter />
</main>
));Islands can also live next to the routes that use them. Place them in a co-located (_islands)/ folder inside routes/. See Routing for the conventions.
Passing props to islands
Passing props to islands is supported, but only if the props are serializable. Props cross from the server to the client, so Fresh has to encode them into the page payload. See Serializable types for the full reference. The short version is that Fresh can serialize the following kinds of values:
- Primitives:
string,number,boolean,bigint,null, andundefined - The special numeric values
NaN,Infinity, and-Infinity Date,RegExp,Map, andSet- Plain objects and arrays of serializable values
- Circular and shared references (serialized once, restored as one instance on the client)
- Preact Signals, if their inner value is serializable
- Serializable functions — plain functions exported from an island file, or
factory(...)wrappers exported from one - JSX elements, passed as a slot (see below)
import { greet } from "../islands/actions.tsx";
<Counter start={3} label="Clicks" tags={new Set(["a", "b"])} onGreet={greet} />;Plain inline closures defined in a route are not serializable, and neither are class instances of unrecognized classes or other arbitrary objects. Passing one throws at render time. See Serializable types for the full set of rules and for how to define your own serializable functions.
Signals stay reactive across islands
A signal passed as a prop is the same reactive signal on the client. This means a parent can own a piece of state and share it across multiple islands. Both islands receive the same count and stay in sync.
import { page } from "./$index.ts";
import { useSignal } from "@preact/signals";
import { Display } from "../islands/Display.tsx";
import { Controls } from "../islands/Controls.tsx";
export default page(() => {
const count = useSignal(0);
return (
<>
<Display count={count} />
<Controls count={count} />
</>
);
});See Signals & reactivity.
Passing JSX to islands
Islands support passing JSX as a “slot”. The static content is rendered on the server and the client splices it back into the island after hydration. This lets you mix server rendered markup into the interactive part of the page without shipping JavaScript for it.
import { ComponentChildren } from "preact";
import { useSignal } from "@preact/signals";
interface Props {
children: ComponentChildren;
}
export function Card({ children }: Props) {
const open = useSignal(true);
return (
<div>
<button onClick={() => (open.value = !open.value)}>{open.value ? "Hide" : "Show"}</button>
{open.value && <div class="card">{children}</div>}
</div>
);
}import { page } from "./$index.ts";
import { Card } from "../islands/Card.tsx";
export default page(() => (
<Card>
<p>This text is rendered on the server.</p>
</Card>
));Nesting islands
There are two ways one island can end up “inside” another, and they behave differently.
Rendered in the outer island’s component body. The outer island imports the inner island and uses it as part of its own JSX. The two run together as a single Preact tree on the client. Only the outer island is a hydration root. The inner island acts as a regular Preact component, and its props are just JavaScript values in the outer island’s runtime, not serialized separately.
import { ComponentChildren } from "preact";
import { useSignal } from "@preact/signals";
import { Counter } from "./Counter.tsx";
export function Card({ children }: { children: ComponentChildren }) {
const open = useSignal(true);
return (
<div>
<button onClick={() => (open.value = !open.value)}>Toggle</button>
{open.value && (
<div class="card">
<Counter />
{children}
</div>
)}
</div>
);
}Passed to the outer island as a slot. The inner island appears in a route file as a child (or other JSX prop) of the outer island. The slot is rendered on the server and spliced into the outer island’s DOM after it hydrates. Preact for the outer island does not control that subtree, so the inner island is a separate hydration root with its own serialized props.
import { page } from "./$index.ts";
import { Card } from "../islands/Card.tsx";
import { Counter } from "../islands/Counter.tsx";
export default page(() => (
<Card>
<Counter start={5} />
<p>Some more server rendered text.</p>
</Card>
));In the first case the outer island bundle carries the inner island’s code, and you can pass anything through props since nothing crosses the boundary again. In the second case the inner island ships its own bundle and its props go through the serializer.
Rendering on the client only
When using client-only APIs like EventSource or navigator.getUserMedia, an island that calls them at render time will fail on the server with something like ReferenceError: EventSource is not defined. Guard the browser-only branch with the IS_BROWSER flag from fresh/runtime.
import { IS_BROWSER } from "fresh/runtime";
export function Live() {
if (!IS_BROWSER) return <div>Loading…</div>;
// Everything here runs only in the browser.
const source = new EventSource("/events");
// ...
return <div>Live!</div>;
}