Islands

Verve pages are static HTML by default. An island is a region you opt into interactivity: it server-renders normally, then a dedicated WASM chunk hydrates it on the client. Chunks load per island — a page with no islands ships no island code.

Declaring an island

Two files, one name. First the registry entry in src/app/islands.zig:

pub const Counter = struct {
    pub const props_schema: []const u8 = "{\"initial\":\"i32\"}";
};

The build walks this file at configure time: every pub const <Name> becomes a manifest entry, and src/client/islands/<Name>.zig compiles to island_<name>.wasm. (Missing implementation? The build falls back to a no-op _default.zig chunk.)

Marking the region

Wrap server-rendered content with verve.island:

const inner = ctx.div().children(.{
    ctx.span().class("count").bind("counter_island").textInt(initial),
    ctx.el("button").attr("z-on-click", "counter_island_bump").text("+"),
});
const island = verve.island(ctx, .{ .name = "Counter" }, inner);

This emits a <verve-island data-name="Counter"> marker. The bridge JS sees it, lazy-loads the chunk, and calls its hydrate export.

The chunk

The real Counter chunk shipped with the framework:

zig
//! Phase 13F — Counter island chunk (shared-runtime).
//!
//! Imports its memory + reactive surface from the main `client.wasm`
//! at instantiation time. The chunk keeps no static state of its own
//! so the chunk + main module can safely share the same linear memory.
//!
//! `hydrate` runs once per `<verve-island data-name="Counter">` on the
//! page. It registers a per-island Signal under the main runtime's
//! root Owner; subsequent click handlers exported from this chunk
//! call into the shared API to mutate it.
//!
//! Multi-instance: `root_id` is a per-page document-order id assigned
//! by the bridge JS. Chunks that need distinct state per
//! `<verve-island>` marker should namespace their bind-names using
//! it — e.g. `std.fmt.bufPrint(&buf, "counter_island_{d}", .{root_id})`.
//! The SSR'd content's `[z-bind=...]` must use the same namespaced
//! form so the on_set hook reaches the right element. This demo
//! ignores `root_id` and uses a single shared signal — multiple
//! `<verve-island data-name="Counter">` markers on the same page
//! would share state (which is what the idempotent `registerI32`
//! contract delivers safely; no slot duplication).

const verve = @import("verve");

const COUNTER_BIND: []const u8 = "counter_island";

export fn hydrate(props_ptr: u32, props_len: u32, root_id: u32) void {
    _ = props_ptr;
    _ = props_len;
    _ = root_id;
    // Allocate the signal in the main runtime's slot table. The
    // matching server-rendered element carries
    // `[z-bind="counter_island"]` so the runtime's `on_set` hook drives
    // the DOM update on every `signalSet*` call below.
    verve.registerI32(COUNTER_BIND, 0);
}

/// Click handler stamped via `[z-on-click="counter_island_bump"]` in
/// the SSR'd island content. The bridge JS string-name delegate looks
/// up the export on the chunk's instance and invokes it directly.
export fn counter_island_bump() void {
    verve.signalSetI32(COUNTER_BIND, verve.signalGetI32(COUNTER_BIND) + 1);
}

hydrate(props_ptr, props_len, root_id) runs once per marker. z-on-click="counter_island_bump" dispatches straight to the chunk's export of that name; the signal write flows through on_set into the bound span.

Typed props

For islands that need server data, declare a Props struct and a matching schema, then encode at render time:

// islands.zig
pub const VizGraph = struct {
    pub const props_schema: []const u8 = "{\"xs\":\"f64[]\",\"ys\":\"f64[]\"}";
    pub const Props = struct {
        xs: []const f64,
        ys: []const f64,
    };
};

// at render time
const props = try verve.encodeProps(ctx, islands.VizGraph.Props{ .xs = xs, .ys = ys });
const island = verve.island(ctx, .{ .name = "VizGraph", .props = props }, inner);

// chunk side
const Props = struct { xs: []const f64, ys: []const f64 };
export fn hydrate(props_ptr: u32, props_len: u32, root_id: u32) void {
    const bytes = @as([*]const u8, @ptrFromInt(props_ptr))[0..props_len];
    const props = verve.decodeProps(Props, bytes, alloc) catch return;
    ...
}

The codec is positional. Fields decode by order, not name — the chunk-side Props must mirror the registry Props field-for-field or hydration silently decodes garbage. Schema wire types: i32, u32, f32, f64, bool, string, and [] variants of each.

Two kinds of chunk

Shared-runtime (Counter style)Stateful (GlDemo style)
Static datanonehas globals/var state
Memoryimports main client.wasm's memoryshares memory, links data at 0x1000
Per pageany numberat most ONE

Stateful chunks all link their data segment at the same base address in the shared linear memory — two on one page clobber each other. The framework's GL demo merges two canvases into one chunk for exactly this reason. Design rule: one island per route, and prefer shared-runtime chunks (keep state in registered signals, not chunk globals).

Multi-instance islands

root_id is a per-page document-order id. Chunks that want distinct state per marker namespace their bind names with it (counter_island_{d}), and the SSR markup must use the same namespaced z-bind. Ignoring root_id (like the Counter above) means all markers share one signal — safe, sometimes even desired.

Initial state without props

ctx.islandState(.{ .count = 5, .label = "hi" }) encodes named primitives (i32/f32/bool/string) into a state blob the chunk reads with verve.islandStateValue(i32, "count") — lighter than the props codec for simple seeds.

Next: Server functions.