Skip to content

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 Xote
2
3module Greeting = {
4 @jsx.component
5 let make = (~name: string, ~emphasis=false) => {
6 <div class={emphasis ? "greeting strong" : "greeting"}>
7 <h1> {Node.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=[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)
2
3<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)
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 {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"])
2
3<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])
6
7<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}
4
5Node.mountById(app(), "app")

In Practice

Example: Counter Component

This example keeps state local to the component and exposes only props.

1open Xote
2
3module Counter = {
4 @jsx.component
5 let make = (~initialValue: int) => {
6 let count = Signal.make(initialValue)
7
8 let increment = (_evt: Dom.event) => {
9 Signal.update(count, n => n + 1)
10 }
11
12 let decrement = (_evt: Dom.event) => {
13 Signal.update(count, n => n - 1)
14 }
15
16 <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}
25
26let 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.keyedList 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