Skip to content

The book · 14

UI extension

book.md · 90 lines · 2 min read

Goal: understand that UI is a compiler extension (not core): .mkoui files, reactivity by decorator, the island model, and the server/client boundary as a network channel.

Makoto’s core does not know what a <div> is. UI is the first official compiler extension: it registers a file type (.mkoui, with its markup grammar), decorators (@state/@component/…), and codegen targets (JS/WASM/SSR). A .mko file never sees markup; whoever does no UI never touches it. Reactivity is volatile by nature (reactive paradigms turn over every few years), and isolating it in an extension lets it age and be swapped without touching the core.

The extension turns on by flag/manifest; markup lives only in .mkoui.

Component = a function that returns markup

Section titled “Component = a function that returns markup”

Props are the parameters; the markup goes in the return {} (there is no implicit return: markup is the content of a return block):

@component
fn Greeting(name: string, count: int) {
return {
<div class="card">
<h1>Hello, {{name}}</h1>
<p>You have {{count}} messages</p>
</div>
}
}

Binding in the markup, by position: {{ }} interpolates content (text via Display, chapter 05); { } binds an expression after an = (prop, dynamic attribute, handler).

@state count := 0 // reactive state: changed → re-renders whoever depends
@derived doubled := count * 2 // recomputes when 'count' changes
@effect { log("count = {{count}}") } // runs when its dependencies change

Each decorator is the core construct plus the reactive plumbing the compiler injects. The model is fine-grained (SolidJS style): each piece of state knows exactly who depends on it. There is no re-execution of the whole component and no Virtual DOM; the diff is direct on the real DOM.

You mark only what is tied to one side; the rest crosses over:

@server fn load_user(id: int) -> User { return db.query(...) } // server only: never goes to the client
@client fn on_scroll() { ... } // client only: runs in the browser (WASM)
fn format_date(d: DateTime) -> string { ... } // unmarked: runs where it is called

Most of the page is static HTML rendered on the server; only the interactive pieces (the “islands”) become WASM on the client, loading only that island’s reactivity. There is no full-page hydration (React’s cost): you send to the browser only what reacts.

The server/client boundary is a network channel

Section titled “The server/client boundary is a network channel”

When an island needs to talk to the server, it is the network channel of chapter 09, not a new UI mechanism. Server and client are two isolated worlds talking over Channel[T] with T + Serializable:

@client
fn LikeButton(post_id: int) {
@state liked := false
fn handle_click() {
chan := net.connect[LikeAction]("/api/like") catch |e| { return }
chan <- LikeAction{ post: post_id }
reply := -> chan timeout(3s) catch |e| { return }
liked = reply.ok
}
return {
<button onclick={handle_click}>{{ liked ? "♥" : "♡" }}</button>
}
}

Scoped CSS (class hashing), @css for external CSS, and @jsimport (JS FFI, under the namespace js.) complete the extension, all restricted to .mkoui.

Next: 15 · Tooling in practice