Skip to content

View

How the View module and JSX components render once and stay reactive over time.

A Xote component is a function that returns a View.node. The component usually runs once, sets up its reactive graph, and then reactive nodes update in place over time.

The recommended path is JSX plus @jsx.component. The function-based View and Html APIs stay available when you need lower-level control or are generating UI programmatically.

View Module

Think in two layers:

  • Static structure: the component function builds the node tree
  • Reactive bindings: signal reads inside reactive nodes, computeds, and effects keep specific parts up to date

Using View

JSX Configuration

To use JSX with Xote, point ReScript at XoteJSX.

1{
2 "dependencies": ["xote"],
3 "jsx": {
4 "version": 4,
5 "module": "XoteJSX"
6 },
7 "compiler-flags": ["-open Xote"]
8}

Writing Components

Recommended Pattern

Use a module with a make function and annotate it with @jsx.component. ReScript derives the props shape from labeled arguments.

1open Xote
2
3module Greeting = {
4 @jsx.component
5 let make = (~name: string, ~emphasis=false) => {
6 <div class={emphasis ? "greeting strong" : "greeting"}>
7 <h1> {View.text(`Hello, ${name}`)} </h1>
8 </div>
9 }
10}
11
12let app = () => {
13 <Greeting name="World" emphasis />
14}

Function API

The lower-level API is still useful when JSX is not a good fit.

1open Xote
2
3let greeting = (name: string) => {
4 Html.div(
5 ~children=[
6 Html.h1(~children=[View.text(`Hello, ${name}`)], ())
7 ],
8 (),
9 )
10}

Reactive Output

JSX expressions are just nodes. For reactive text, use View.signalText. For arrays and lists, prefer View.eachWithKey when item identity matters.

1let count = Signal.make(0)
2
3<div>
4 {View.signalText(() => `Count: ${Signal.get(count)->Int.toString}`)}
5</div>

Attributes and Events

In JSX, common HTML props are exposed directly. In the function API, use View.Attr.string, View.Attr.signal, and View.Attr.compute.

1let isActive = Signal.make(false)
2
3let toggle = (_evt: Dom.event) => {
4 Signal.update(isActive, active => !active)
5}
6
7<button
8 class={Signal.get(isActive) ? "btn active" : "btn"}
9 onClick={toggle}>
10 {View.text("Toggle")}
11</button>
1Html.button(
2 ~attrs=[
3 View.Attr.compute("class", () =>
4 Signal.get(isActive) ? "btn active" : "btn"
5 ),
6 ],
7 ~events=[("click", toggle)],
8 ~children=[View.text("Toggle")],
9 (),
10)

In JSX, use class, not className. Use type_ for the HTML type attribute because type is reserved in ReScript.

Lists

Use View.each for simple arrays that can be fully re-rendered, and View.eachWithKey when item identity matters.

1let items = Signal.make(["Apple", "Banana", "Cherry"])
2
3<ul>
4 {View.each(items, item => <li> {View.text(item)} </li>)}
5</ul>
1type todo = {id: string, text: string}
2let todos = Signal.make([
3 {id: "1", text: "Write docs"},
4 {id: "2", text: "Ship release"},
5])
6
7<ul>
8 {View.eachWithKey(
9 todos,
10 todo => todo.id,
11 todo => <li> {View.text(todo.text)} </li>,
12 )}
13</ul>

Choose stable keys. Database IDs and route slugs are good. Array indexes are not.

Mounting

Use View.mount when you already have a DOM element, or View.mountById when you want to look one up by id.

1let app = () => {
2 <div> {View.text("Hello, Xote")} </div>
3}
4
5View.mountById(app(), "app")

In Practice

Example: Todo List

This example keeps the shape simple while still showing composition: an input form, a summary, and a keyed list of items all built from small components.

TodoList.res
1open Xote
2
3type todo = {
4 id: string,
5 title: string,
6 done: bool,
7}
8
9let todos = Signal.make([
10 {id: "1", title: "Write the View guide", done: true},
11 {id: "2", title: "Add a runnable example", done: false},
12])
13
14let draft = Signal.make("")
15
16module TodoComposer = {
17 @jsx.component
18 let make = () => {
19 <div>
20 <input onInput={handleInput} value={() => Signal.get(draft)} />
21 <button onClick={addTodo}> {View.text("Add")} </button>
22 </div>
23 }
24}
25
26module TodoRow = {
27 @jsx.component
28 let make = (~todo: todo) => {
29 <li>
30 <span> {View.text(todo.title)} </span>
31 <input type_="checkbox" checked={todo.done} />
32 </li>
33 }
34}
35
36@jsx.component
37let make = () => {
38 <div>
39 <TodoComposer />
40 <ul>
41 {View.eachWithKey(todos, todo => todo.id, todo => <TodoRow todo />)}
42 </ul>
43 </div>
44}
View composition

Todo list

One signal drives a small set of focused components.

2 tasks left
3 total
  • Learn ReScript
  • Learn signals
  • Write my first Xote component

Working Style

Best Practices

  • Default to the JSX module pattern when there is no reason to drop lower.
  • Keep state close to where it is used. Local signals are cheap and usually easier to follow.
  • Use View.eachWithKey for collections that reorder, insert, or preserve local DOM state.
  • Be explicit about reactive output so the update boundaries stay readable in the component.

Next Steps