Server and client
The boundary is a directive on the file, and a marker at the call site.
The boundary
A // @runsOn server component may fetch data with await data(...) and render client components. Crossing the boundary requires an explicit marker at the JSX call site:
// @runsOn server
import { data } from '@marisjs/runtime';
import { Stats } from '../islands/Stats.tsx';
type Props = {};
export function Dashboard(props: Props) {
const products = await data(async () => {
const res = await fetch('https://api.example.com/products');
return res.json();
});
return (
<div>
<h1>Dashboard</h1>
<Stats items={products} client:hydrate />
</div>
);
} Everything left of the marker is static HTML produced at build time. Everything right of it is a compiled island module, hydrated in place when the page loads.
What hydration means here
- Only components marked
client:hydrateship JavaScript to the browser. - The page's compiled module imports exactly the islands it uses - nothing else.
- Each island mounts onto its own element and manages its own signals.
- A failed island module does not take down the rest of the page's islands.
Props cross the boundary: island props are serialized through SSR into the placeholder's
data-props attribute - dynamic values from data() and expressions included. Every instance mounts with its own payload, and using the same island twice on one page is fine: one import, N mounts. One-way street: server files may import client files, but a client file importing a server component is a hard build error (
CLIENT_IMPORTS_SERVER). Server data flows into islands via props, not imports.