Skip to content

Components

The Xote component system for building reactive user interfaces.

Components Overview

Xote provides a lightweight component system for building reactive UIs. Components are functions that return virtual nodes, which are then rendered to the DOM.

Xote supports two syntax styles for building components:

  • JSX Syntax: Modern, declarative JSX syntax (recommended)
  • Function API: Explicit function calls with labeled parameters

What are Components?

In Xote, a component is simply a function that returns a Node.node. The recommended way to define components is with the component module pattern using @jsx.component:

Component Module Pattern (Recommended)

Use @jsx.component to define components as modules with a make function. This decorator automatically generates the props type from labeled arguments, enabling clean JSX usage:

open Xote

module Greeting = {
  @jsx.component
  let make = (~name: string) => {
    <div>
      <h1> {Node.text("Hello, " ++ name ++ "!")} </h1>
    </div>
  }
}

// Usage in JSX:
<Greeting name="World" />

Components without props simply omit the labeled arguments:

module Header = {
  @jsx.component
  let make = () => {
    <header>
      <h1> {Node.text("My App")} </h1>
    </header>
  }
}

// Usage:
<Header />

Key points:

  • Components are defined as modules with a make function
  • The @jsx.component decorator transforms labeled arguments (~propName) into a props record type automatically
  • Components are used in JSX with <ComponentName prop={value} /> syntax
  • File-level modules can also be components — just add @jsx.component to a top-level make function

Plain JSX Syntax

You can also define components as simple functions without the decorator:

open Xote

let greeting = () => {
  <div>
    <h1> {Node.text("Hello, Xote!")} </h1>
  </div>
}

Function API

open Xote

let greeting = () => {
  Html.div(
    ~children=[
      Html.h1(~children=[Node.text("Hello, Xote!")], ())
    ],
    ()
  )
}

JSX Configuration

To use JSX syntax, configure your rescript.json:

{
  "bs-dependencies": ["xote"],
  "jsx": {
    "version": 4,
    "module": "XoteJSX"
  },
  "compiler-flags": ["-open Xote"]
}

Text Nodes

Static Text

Use Node.text() for static text:

<div>
  {Node.text("This text never changes")}
</div>

Reactive Text

Use Node.signalText() for text that updates with signals:

let count = Signal.make(0)

<div>
  {Node.signalText(() =>
    "Count: " ++ Int.toString(Signal.get(count))
  )}
</div>

The function is tracked, so the text automatically updates when count changes.

Attributes

JSX Props

JSX elements support common HTML attributes:

  • class - CSS classes (note: class, not className)
  • id - Element ID
  • style - Inline styles
  • type_ - Input type (with underscore to avoid keyword conflict)
  • value - Input value
  • placeholder - Input placeholder
  • disabled - Boolean disabled state
  • checked - Boolean checked state
<button
  class="btn btn-primary"
  type_="button"
  disabled={true}>
  {Node.text("Submit")}
</button>

Static Attributes (Function API)

Html.button(
  ~attrs=[
    Node.attr("class", "btn btn-primary"),
    Node.attr("type", "button"),
    Node.attr("disabled", "true"),
  ],
  ()
)

Reactive Attributes

Function API supports reactive attributes:

let isActive = Signal.make(false)

Html.div(
  ~attrs=[
    Node.computedAttr("class", () =>
      Signal.get(isActive) ? "active" : "inactive"
    )
  ],
  ()
)

Event Handlers

JSX Event Props

JSX elements support common event handlers:

  • onClick - Click events
  • onInput - Input events
  • onChange - Change events
  • onSubmit - Form submit events
  • onFocus, onBlur - Focus events
  • onKeyDown, onKeyUp - Keyboard events
let count = Signal.make(0)

let increment = (_evt: Dom.event) => {
  Signal.update(count, n => n + 1)
}

<button onClick={increment}>
  {Node.text("+1")}
</button>

Lists

Simple Lists (Non-Keyed)

Use Node.list() for simple lists where the entire list re-renders on any change:

let items = Signal.make(["Apple", "Banana", "Cherry"])

<ul>
  {Node.list(items, item =>
    <li> {Node.text(item)} </li>
  )}
</ul>

Note: Simple lists re-render completely when the array changes (no diffing). For better performance, use keyed lists.

Keyed Lists (Efficient Reconciliation)

Use Node.listKeyed() for efficient list rendering with DOM element reuse:

type todo = {id: int, text: string, completed: bool}
let todos = Signal.make([
  {id: 1, text: "Buy milk", completed: false},
  {id: 2, text: "Walk dog", completed: true},
])

<ul>
  {Node.listKeyed(
    todos,
    todo => todo.id->Int.toString,  // Key extractor
    todo => <li> {Node.text(todo.text)} </li>  // Renderer
  )}
</ul>

Benefits of keyed lists:

  • Reuses DOM elements - Only updates what changed
  • Preserves component state - When list items move position
  • Better performance - Fewer DOM operations for large lists
  • Efficient reconciliation - Adds/removes/moves only necessary elements

Best practices:

  • Always use unique, stable keys (like database IDs)
  • Don't use array indices as keys
  • Keys should be strings
  • Use listKeyed for any list that can be reordered, filtered, or modified

Mounting to the DOM

Use mountById to attach your component to an existing DOM element:

let app = () => {
  <div> {Node.text("Hello, World!")} </div>
}

Node.mountById(app(), "app")

Example: Counter Component

Here's a complete counter component using the component module pattern:

open Xote

module Counter = {
  @jsx.component
  let make = (~initialValue: int) => {
    let count = Signal.make(initialValue)

    let increment = (_evt: Dom.event) => {
      Signal.update(count, n => n + 1)
    }

    let decrement = (_evt: Dom.event) => {
      Signal.update(count, n => n - 1)
    }

    <div class="counter">
      <h2>
        {Node.signalText(() =>
          "Count: " ++ Int.toString(Signal.get(count))
        )}
      </h2>
      <div class="controls">
        <button onClick={decrement}>
          {Node.text("-")}
        </button>
        <button onClick={increment}>
          {Node.text("+")}
        </button>
      </div>
    </div>
  }
}

// Use the component in JSX
module App = {
  @jsx.component
  let make = () => {
    <Counter initialValue={10} />
  }
}

Node.mountById(App.make({}), "app")

Best Practices

  • Keep components small: Each component should do one thing well
  • Use signals for local state: Create signals inside components for component-specific state
  • Pass data via props: Use record types for component parameters
  • Compose components: Build complex UIs from simple, reusable components
  • Choose the right list type: Use listKeyed for dynamic lists, list for simple static lists
  • Use class not className: In JSX, use the class prop for CSS classes

Next Steps

  • Try the Demos to see components in action
  • Learn about Routing for building SPAs
  • Explore the API Reference for detailed documentation
Was this page helpful?