# Chapter 16: Reactive UI

> ⚠ **Planned — not in Loup 1.25.** This chapter describes the design in the Language Reference (§2-4.1–§2-4.7, with the template fences of §1-16). The toolchain does not implement it yet, so its listings are design sketches and are not verified.

The Reference specifies a fine-grained reactive UI: signals hold state, components are functions that return a `View`, and a browser runtime renders views to the DOM and updates only the parts that read a changed signal. A component's body runs once, as in SolidJS, not on every render as in React.

None of this ships in Loup 1.25: there is no `ui` module, no `View`, no `mount` and no browser runtime. The import alone stops `loup run`:

```text
main.lpweb:3:6: error[E0001]: 'ui' is not a standard-library module (namespace 'ui' is reserved for the standard library)
```

What works today is rendering an `html` fence to a string (Chapter 14).

## Signals

`ui` declares three functions (§2-4.1): `signal(initial)` makes a writable `Signal[T]` with `get()`, `set(value)` and `update(fn)`; `derived(fn)` makes a read-only signal recomputed when the signals `fn` reads change; `effect(fn)` runs `fn` now and again after each change to a signal it read, and returns an `EffectHandle` whose `dispose()` stops it.

```loup,ignore
@mode(web.compiled)

from ui import (signal, derived, effect, Signal, EffectHandle);

func cartTotals(): void {
    let price: Signal[int] = signal(120);
    let qty = signal(3);
    let total = derived(func(): int => price.get() * qty.get());

    let handle: EffectHandle = effect(func(): void {
        println(i"total is {$ total.get() $}");
    });                                           # runs now: total is 360

    qty.set(4);                                   # total is 480
    price.update(func(p: int): int => p - 20);    # total is 400

    handle.dispose();
    qty.set(5);                                   # no output
}
```

*Listing 16-1: Signals, a derived value and an effect (design sketch)*

A `get()` inside reactive code records a dependency; a write re-evaluates the dependents. Earlier drafts of this book called `derived` `computed`; the Reference's name is `derived`.

## Components

A component is a function returning a `View`, the immutable UI description that `html` and `ui` fences produce (§2-4.2, §2-4.4). Components have no lifecycle methods: setup runs once, and signal reads inside the fence keep the output current.

````loup,ignore
@mode(web.compiled)

from ui import (signal, View);

pub func Counter(start: int): View {
    let count = signal(start);
    return ```(html):(none)
    <div class="counter">
      <p>Count: {$ count.get() $}</p>
      <button onclick={$ func(): void { count.update(func(n: int): int => n + 1); } $}>+1</button>
      <button onclick={$ func(): void { count.set(start); } $}>Reset</button>
    </div>
    ```;
}
````

*Listing 16-2: A counter component (design sketch)*

An `on*` attribute takes a Loup callable, not JavaScript. The compiler replaces it with `data-loup-ev="click:__ev_…"` and registers the closure; the runtime attaches it with `addEventListener`, so no inline script reaches the page and no `'unsafe-inline'` CSP is needed (Chapter 19). The Reference specifies `E_INLINE_JS_EVENT_HANDLER` for a string handler such as `onclick="count++"`, and `E_COMPONENT_NOT_FOUND` / `E_COMPONENT_NOT_VIEW` for bad component tags.

## Mounting

`mount(view, target)` attaches a `View` to a root and returns a `MountHandle` with `unmount()` (§2-4.3). On the browser the target is a selector string or an `Element`.

```loup,ignore
@mode(web.scripted)

from ui import (mount);
from counter import (Counter);

let handle = mount(Counter(start = 0), "#root");
```

*Listing 16-3: The entry file (design sketch)*

The entry is `web.scripted` because the bootstrap expects a top-level `mount` call (§1-20.6), and a `web.compiled` file rejects top-level statements with `E_TOPLEVEL_CONTROL_AOT` (Chapter 2). The Reference does not yet say how a `web.compiled` entry registers its `mount`.

## The `ui` fence

A `ui` fence builds a `View` from platform-neutral PascalCase primitives that the web backend maps to HTML and CSS and a desktop backend to native widgets (§2-4.6): `Row`, `Column`, `Stack`, `Scroll`, `Spacer`, `Divider`, `Padding`; `Text`, `Button`, `Image`, `Icon`, `Canvas`; and the inputs `TextField`, `Checkbox`, `Slider`, `Select[T]`, each bound two-way to a `Signal`.

````loup,ignore
@mode(web.compiled)

from ui import (signal, ComponentRegistry, View, TextStyle);
import ui.primitives;

pub func TodoPanel(): View {
    let registry = ComponentRegistry.of(ui.primitives.components);
    let items = signal(["buy milk"]);
    let draft = signal("");
    return ```(ui):(components=registry)
    <Column spacing={$ 12 $}>
      <Text style={$ TextStyle(size = 20, weight = 600) $}>Today</Text>
      <Row spacing={$ 8 $}>
        <TextField value={$ draft $} placeholder="Add an item" />
        <Button onclick={$ func(): void {
            items.update(func(xs: list[string]): list[string] => xs + [draft.get()]);
            draft.set("");
        } $}>
          <Text>Add</Text>
        </Button>
      </Row>
      {% for (item in items.get()) %}
        <Text>{$ item $}</Text>
      {% end %}
    </Column>
    ```;
}
````

*Listing 16-4: A to-do panel in a `ui` fence (design sketch)*

The `components=registry` descriptor names a `ComponentRegistry` in lexical scope (§2-4.2). The sketch builds it inside the function because a top-level `let` in `web.compiled` needs a constant initializer. A lowercase tag in a `ui` fence is `E_UI_LOWERCASE_TAG`.

## Async components

A component may be an `async func` returning `Task[View]`; `{% suspend fallback={$ … $} %}` shows the fallback until every async component inside the block has finished, and several of them load concurrently (§2-4.5). Note that `async` functions run eagerly on today's interpreter (Chapter 15).

## Summary

- Reactive UI (Language Reference §2-4) is not in Loup 1.25: no `ui` module, `View`, `mount` or browser runtime.
- `signal`, `derived` and `effect` are the primitives; reads are dependencies, writes re-evaluate dependents.
- Components are functions returning `View`; they run once, and event handlers are Loup closures wired through `data-loup-ev`.
- `mount(view, "#root")` returns a `MountHandle`; the specified entry is a top-level call, which only `web.scripted` allows.
- `ui` fences use PascalCase primitives from a `ComponentRegistry`; `{% suspend %}` covers async components.
