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
Recommended Pattern
Use a module with a make function and annotate it with @jsx.component. ReScript derives the props shape from labeled arguments.
1open Xote23module Greeting = {4 @jsx.component5 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}1112let app = () => {13 <Greeting name="World" emphasis />14}
JSX Components
1open Xote23module Greeting = {4 @jsx.component5 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)23<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.component2let 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)23let toggle = (_evt: Dom.event) => {4 Signal.update(isActive, active => !active)5}67<button8 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"])23<ul>4 <View.For5 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])67<ul>8 <View.For9 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>45<View.Maybe6 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/>1011<View.Value12 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/>2021<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}45View.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.
1open Xote23type todo = {4id: string,5title: string,6done: bool,7}89let todos = Signal.make([10{id: "1", title: "Write the View guide", done: true},11{id: "2", title: "Add a runnable example", done: false},12])1314let draft = Signal.make("")1516module TodoComposer = {17@jsx.component18let 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}2526module TodoRow = {27@jsx.component28let 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}3536@jsx.component37let make = () => {38<div>39 <TodoComposer />40 <ul>41 <View.For42 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
bytoView.Forfor 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
- Read Router when these components need client-side navigation.
- Read Server-Side Rendering if the same components need to render on the server.