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 docs use JSX for examples because it keeps component structure close to the HTML it produces. The lower-level View API still exists for runtime primitives such as reactive text, keyed lists, and mounting.

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

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}`} </View.Text> </h1>
8 </div>
9 }
10}
11
12let app = () => {
13 <Greeting name="World" emphasis />
14}

JSX Components

1open Xote
2
3module Greeting = {
4 @jsx.component
5 let make = (~name: string) => {
6 <section class="greeting-card">
7 <h2> <View.Text> {`Hello, ${name}`} </View.Text> </h2>
8 <p> <View.Text> "This component is ordinary ReScript plus JSX." </View.Text> </p>
9 </section>
10 }
11}

Reactive Output

JSX expressions are just nodes. For direct value output, use primitives such as View.Text, View.Int, View.Float, and View.Bool. Their children can be raw values, signals, Prop.t values, or computed functions. For arrays and lists, use View.For; pass by when item identity matters.

1let count = Signal.make(0)
2
3<div>
4 <View.Text> "Count: " </View.Text>
5 <View.Int> {count} </View.Int>
6</div>

When a formatted string depends on a value that can be static or reactive, keep the prop as Prop.t and read it inside a function child.

1@jsx.component
2let make = (~name: Prop.t<string>) => {
3 <View.Text> {() => `Hello, ${Prop.get(name)}`} </View.Text>
4}

Attributes and Events

In JSX, common HTML props are exposed directly. Pass functions for reactive attributes when the value should be recomputed.

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" </View.Text>
11</button>

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

Lists

Use View.For for simple arrays that can be fully re-rendered. Add by when item identity matters. View.For accepts Prop.static(value) for plain data and Prop.signal(signal) for reactive data.

1let items = Signal.make(["Apple", "Banana", "Cherry"])
2
3<ul>
4 <View.For
5 each={Prop.signal(items)}
6 render={item => <li> <View.Text> {item} </View.Text> </li>}
7 />
8</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.For
9 each={Prop.signal(todos)}
10 by={todo => todo.id}
11 render={todo => <li> <View.Text> {todo.text} </View.Text> </li>}
12 />
13</ul>

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

Conditional Output

Use View.Show for boolean branches, View.Maybe for option values, and View.Value when a whole node should be re-rendered from one static or reactive value. Use View.Text, View.Int, View.Float, and View.Bool for direct value output.

1<View.Show when_={Prop.signal(isReady)} fallback={<p> <View.Text> "Loading" </View.Text> </p>}>
2 <p> <View.Text> "Ready" </View.Text> </p>
3</View.Show>
4
5<View.Maybe
6 value={Prop.signal(selectedTodo)}
7 fallback={<p> <View.Text> "No todo selected" </View.Text> </p>}
8 render={todo => <p> <View.Text> {todo.text} </View.Text> </p>}
9/>
10
11<View.Value
12 value={Prop.signal(count)}
13 render={count =>
14 <p>
15 <View.Text> "Count: " </View.Text>
16 <View.Int> {count} </View.Int>
17 </p>
18 }
19/>
20
21<p>
22 <View.Text> "Count: " </View.Text>
23 <View.Int> {count} </View.Int>
24 <View.Text> ", ready: " </View.Text>
25 <View.Bool> {isReady} </View.Bool>
26</p>

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" </View.Text> </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 = {
4id: string,
5title: string,
6done: 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
18let make = () => {
19 <div>
20 <input onInput={handleInput} value={() => Signal.get(draft)} />
21 <button onClick={addTodo}> <View.Text> "Add" </View.Text> </button>
22 </div>
23}
24}
25
26module TodoRow = {
27@jsx.component
28let make = (~todo: todo) => {
29 <li>
30 <span> <View.Text> {todo.title} </View.Text> </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.For
42 each={Prop.signal(todos)}
43 by={todo => todo.id}
44 render={todo => <TodoRow todo />}
45 />
46 </ul>
47</div>
48}

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.
  • Pass by to View.For for JSX 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