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:
//! 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
Propsmust mirror the registryPropsfield-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 data | none | has globals/var state |
| Memory | imports main client.wasm's memory | shares memory, links data at 0x1000 |
| Per page | any number | at 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.