Components
How Xote components render once and stay reactive over time.
A Xote component is a function that returns a Node.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 Node and Html APIs stay available when you need lower-level control or are generating UI programmatically.
Component Model
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
Building Components
JSX Configuration
To use JSX with Xote, point ReScript at XoteJSX.
1{2 "bs-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> {Node.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=[Node.text("Hello, " ++ name)], ())7 ],8 (),9 )10}Reactive Output
JSX expressions are just nodes. For reactive text, use Node.signalText. For arrays of reactive children, use Node.signalFragment or one of the list helpers.
1let count = Signal.make(0)23<div>4 {Node.signalText(() => "Count: " ++ Int.toString(Signal.get(count)))}5</div>Attributes and Events
In JSX, common HTML props are exposed directly. In the function API, use Node.attr, Node.signalAttr, and Node.computedAttr.
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 {Node.text("Toggle")}11</button>1Html.button(2 ~attrs=[3 Node.computedAttr("class", () =>4 Signal.get(isActive) ? "btn active" : "btn"5 ),6 ],7 ~events=[("click", toggle)],8 ~children=[Node.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 Node.list for simple arrays that can be fully re-rendered, and Node.keyedList when item identity matters.
1let items = Signal.make(["Apple", "Banana", "Cherry"])23<ul>4 {Node.list(items, item => <li> {Node.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 {Node.keyedList(9 todos,10 todo => todo.id,11 todo => <li> {Node.text(todo.text)} </li>,12 )}13</ul>Choose stable keys. Database IDs and route slugs are good. Array indexes are not.
Mounting
Use Node.mount when you already have a DOM element, or Node.mountById when you want to look one up by id.
1let app = () => {2 <div> {Node.text("Hello, Xote")} </div>3}45Node.mountById(app(), "app")In Practice
Example: Counter Component
This example keeps state local to the component and exposes only props.
1open Xote23module Counter = {4 @jsx.component5 let make = (~initialValue: int) => {6 let count = Signal.make(initialValue)78 let increment = (_evt: Dom.event) => {9 Signal.update(count, n => n + 1)10 }1112 let decrement = (_evt: Dom.event) => {13 Signal.update(count, n => n - 1)14 }1516 <div class="counter">17 <h2>18 {Node.signalText(() => "Count: " ++ Int.toString(Signal.get(count)))}19 </h2>20 <button onClick={decrement}> {Node.text("-")} </button>21 <button onClick={increment}> {Node.text("+")} </button>22 </div>23 }24}2526let app = () => {27 <Counter initialValue={10} />28}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
Node.keyedListfor 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.