docs/latest/concepts/islands.md
Islands enable client side interactivity in Fresh and they are rendered both on the server and in the client.
Islands are defined by creating a file in the islands/ folder or a
(_islands) folder somewhere in the routes/ directory. The name of this file
must be a PascalCase or kebab-case name of the island.
import { useSignal } from "@preact/signals";
export default function MyIsland() {
const count = useSignal(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => (count.value += 1)}>+</button>
</div>
);
}
An island can be used anywhere like a regular Preact component. Fresh will take care of making it interactive on the client.
import { App, staticFiles } from "fresh";
import MyIsland from "./islands/my-island.tsx";
const app = new App()
.use(staticFiles())
.get("/", (ctx) => ctx.render(<MyIsland />));
Passing props to islands is supported, but only if the props are serializable. Fresh can serialize the following types of values:
string, number, boolean, bigint, undefined, and
nullInfinity, -Infinity, -0, and NaNUint8ArrayURLDateRegExpJSX ElementsMap and SetTemporal objects (Instant, ZonedDateTime, PlainDate, PlainTime,
PlainDateTime, PlainYearMonth, PlainMonthDay, Duration)Circular references are supported. If an object or signal is referenced multiple times, it is only serialized once and the references are restored upon deserialization.
[warn]: Passing functions to an island is not supported.
tsxexport default function () { // WRONG return <MyIsland onClick={() => console.log("hey")} />; }
A powerful feature of Fresh is that you can pass server-rendered JSX to an island via props.
import { staticFiles } from "fresh";
import MyIsland from "../islands/my-island.tsx";
const app = new App()
.use(staticFiles())
.get("/", (ctx) => {
return ctx.render(
<MyIsland jsx={<h1>hello</h1>}>
<p>This text is rendered on the server</p>
</MyIsland>,
);
});
Islands can be nested within other islands as well. In that scenario they act like a normal Preact component, but still receive the serialized props if any were present.
In essence, Fresh allows you to mix static and interactive parts in your app in a way that's most optimal for your app. We'll keep sending only the JavaScript that is needed for the islands to the browser.
export default (props: { foo: string }) => <>{props.foo}</>;
import MyIsland from "../islands/my-island.tsx";
import OtherIsland from "../islands/other-island.tsx";
// Later...
<div>
<MyIsland>
<OtherIsland foo="this prop will be serialized" />
</MyIsland>
<p>Some more server rendered text</p>
</div>;
When using client-only APIs, like EventSource or navigator.getUserMedia, the
component would error during server-side rendering. Use the IS_BROWSER
constant from fresh/runtime to guard browser-only code. It is false on the
server and true in the browser:
import { IS_BROWSER } from "fresh/runtime";
export function MyIsland() {
// Return any prerenderable JSX here which makes sense for your island
if (!IS_BROWSER) return <div></div>;
// All the code which must run in the browser comes here!
// Like: EventSource, navigator.getUserMedia, etc.
return <div></div>;
}
Custom elements
can be used in Fresh, but they must be registered client-side since
customElements.define() is a browser API.
Use an island to register and render custom elements:
import { useEffect } from "preact/hooks";
import { IS_BROWSER } from "fresh/runtime";
export function MyElement() {
useEffect(() => {
if (customElements.get("my-greeting")) return;
customElements.define(
"my-greeting",
class extends HTMLElement {
connectedCallback() {
const name = this.getAttribute("name") ?? "World";
this.innerHTML = `<p>Hello, ${name}!</p>`;
}
},
);
}, []);
if (!IS_BROWSER) {
return <div></div>;
}
return <my-greeting name="Fresh" />;
}
Third-party web component libraries work the same way - import and register them inside an island:
import { useEffect } from "preact/hooks";
import { IS_BROWSER } from "fresh/runtime";
export function ShoelaceButton() {
useEffect(() => {
// Import the library's registration script
import("@shoelace-style/shoelace/dist/components/button/button.js");
}, []);
if (!IS_BROWSER) {
return <button>Click me</button>;
}
return <sl-button variant="primary">Click me</sl-button>;
}
[tip]: Return a plain HTML fallback from the server-side branch (
!IS_BROWSER) so the page is usable before JavaScript loads.