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 Xote23module Greeting = {4 @jsx.component5 let make = (~name: string, ~emphasis=false) => {6 <div class={emphasis ? "greeting strong" : "greeting"}>7 <h1> {View.text(`Hello, ${name}`)} </h1>8 </div>9 }10}1112let app = () => {13 <Greeting name="World" emphasis />14}Function API
The lower-level API is still useful when JSX is not a good fit.
1open Xote23let 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)23<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)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")}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"])23<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])67<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}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 = {4 id: string,5 title: string,6 done: 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.component18 let make = () => {19 <div>20 <input onInput={handleInput} value={() => Signal.get(draft)} />21 <button onClick={addTodo}> {View.text("Add")} </button>22 </div>23 }24}2526module TodoRow = {27 @jsx.component28 let make = (~todo: todo) => {29 <li>30 <span> {View.text(todo.title)} </span>31 <input type_="checkbox" checked={todo.done} />32 </li>33 }34}3536@jsx.component37let make = () => {38 <div>39 <TodoComposer />40 <ul>41 {View.eachWithKey(todos, todo => todo.id, todo => <TodoRow todo />)}42 </ul>43 </div>44}Todo list
One signal drives a small set of focused components.
- 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.eachWithKeyfor 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.