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 Component.node:
JSX Syntax
open Xote
let greeting = () => {
<div>
<h1> {Component.text("Hello, Xote!")} </h1>
</div>
}
Function API
open Xote
let greeting = () => {
Component.div(
~children=[
Component.h1(~children=[Component.text("Hello, Xote!")], ())
],
()
)
}
JSX Configuration
To use JSX syntax, configure your rescript.json:
{
"jsx": {
"version": 4,
"module": "Xote__JSX"
}
}
Virtual Node Types
Xote uses several node types to represent UI elements:
- Element: Standard DOM elements (
div,button,input, etc.) - Text: Static text nodes
- SignalText: Reactive text that updates when signals change
- Fragment: Groups multiple nodes without a wrapper element
- SignalFragment: Reactive fragment that re-renders when a signal changes
Creating Elements
Use helper functions for common HTML elements:
Component.div(~attrs=?, ~events=?, ~children=?, ())
Component.button(~attrs=?, ~events=?, ~children=?, ())
Component.input(~attrs=?, ~events=?, ())
Component.h1(~attrs=?, ~children=?, ())
Component.p(~attrs=?, ~children=?, ())
// ... and many more
All elements accept optional parameters:
~attrs: Array of attributes (static or reactive)~events: Array of event listeners~children: Array of child nodes
Text Nodes
Static Text
Use Component.text() for static text:
JSX:
<div>
{Component.text("This text never changes")}
</div>
Function API:
Component.div(
~children=[
Component.text("This text never changes")
],
()
)
Reactive Text
Use Component.textSignal() for text that updates with signals:
JSX:
let count = Signal.make(0)
<div>
{Component.textSignal(() =>
"Count: " ++ Int.toString(Signal.get(count))
)}
</div>
Function API:
let count = Signal.make(0)
Component.div(
~children=[
Component.textSignal(() =>
"Count: " ++ Int.toString(Signal.get(count))
)
],
()
)
The function is tracked, so the text automatically updates when count changes.
Attributes
Xote provides a unified attributes API with helper functions.
JSX Props
JSX elements support common HTML attributes:
class- CSS classes (note:class, notclassName)id- Element IDstyle- Inline stylestype_- Input type (with underscore to avoid keyword conflict)value- Input valueplaceholder- Input placeholderdisabled- Boolean disabled statechecked- Boolean checked statehref- Link URLtarget- Link target
JSX Example:
<button
class="btn btn-primary"
type_="button"
disabled={true}>
{Component.text("Submit")}
</button>
Static Attributes (Function API)
Component.button(
~attrs=[
Component.attr("class", "btn btn-primary"),
Component.attr("type", "button"),
Component.attr("disabled", "true"),
],
()
)
Reactive Attributes with Signals
JSX:
let isActive = Signal.make(false)
let activeClass = Computed.make(() =>
Signal.get(isActive) ? "active" : "inactive"
)
<div class={Signal.peek(activeClass)}>
{Component.text("Content")}
</div>
Function API:
let isActive = Signal.make(false)
Component.div(
~attrs=[
Component.signalAttr("class", isActive->Signal.map(active =>
active ? "active" : "inactive"
))
],
()
)
Reactive Attributes with Computed Functions
Function API:
let count = Signal.make(0)
Component.button(
~attrs=[
Component.computedAttr("disabled", () =>
Signal.get(count) >= 10 ? "true" : ""
)
],
()
)
Mixing Static and Reactive
Function API:
Component.button(
~attrs=[
Component.attr("type", "button"), // Static
Component.computedAttr("class", () => // Reactive
Signal.get(isActive) ? "active" : "inactive"
),
Component.attr("aria-label", "Toggle"), // Static
],
()
)
Event Handlers
JSX Event Props
JSX elements support common event handlers:
onClick- Click eventsonInput- Input eventsonChange- Change eventsonSubmit- Form submit eventsonFocus- Focus eventsonBlur- Blur eventsonKeyDown/onKeyUp- Keyboard eventsonMouseEnter/onMouseLeave- Mouse hover events
JSX Example:
let count = Signal.make(0)
let increment = (_evt: Dom.event) => {
Signal.update(count, n => n + 1)
}
<button onClick={increment}>
{Component.text("+1")}
</button>
Multiple events in JSX:
let handleClick = (_evt: Dom.event) => Console.log("Clicked")
let handleMouseEnter = (_evt: Dom.event) => Console.log("Hover")
<button
onClick={handleClick}
onMouseEnter={handleMouseEnter}>
{Component.text("Hover me")}
</button>
Function API
Attach DOM event listeners using the ~events parameter:
let count = Signal.make(0)
let increment = (_evt: Dom.event) => {
Signal.update(count, n => n + 1)
}
Component.button(
~events=[("click", increment)],
~children=[Component.text("+1")],
()
)
Multiple events:
let handleClick = (_evt: Dom.event) => Console.log("Clicked")
let handleMouseOver = (_evt: Dom.event) => Console.log("Hover")
Component.button(
~events=[
("click", handleClick),
("mouseover", handleMouseOver),
],
()
)
Lists
Xote provides two approaches for rendering lists:
Simple Lists (Non-Keyed)
Use Component.list() for simple lists where the entire list re-renders on any change:
JSX:
let items = Signal.make(["Apple", "Banana", "Cherry"])
<ul>
{Component.list(items, item =>
<li> {Component.text(item)} </li>
)}
</ul>
Function API:
let items = Signal.make(["Apple", "Banana", "Cherry"])
Component.ul(
~children=[
Component.list(items, item =>
Component.li(
~children=[Component.text(item)],
()
)
)
],
()
)
Note: Simple lists re-render completely when the array changes (no diffing). For better performance, use keyed lists.
Keyed Lists (Efficient Reconciliation)
Use Component.listKeyed() for efficient list rendering with DOM element reuse:
JSX:
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>
{Component.listKeyed(
todos,
todo => todo.id->Int.toString, // Key extractor
todo => <li> {Component.text(todo.text)} </li> // Renderer
)}
</ul>
Function API:
type todo = {id: int, text: string, completed: bool}
let todos = Signal.make([...])
Component.ul(
~children=[
Component.listKeyed(
todos,
todo => todo.id->Int.toString, // Key extractor
todo => Component.li(
~children=[Component.text(todo.text)],
()
)
)
],
()
)
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
- Correct animations - Essential for transitions and animations
- 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
listKeyedfor any list that can be reordered, filtered, or modified
Fragments
Group nodes without adding a wrapper element:
let header = () => {
Component.fragment([
Component.h1(~children=[Component.text("Title")], ()),
Component.p(~children=[Component.text("Subtitle")], ()),
])
}
Mounting to the DOM
Use mountById to attach your component to an existing DOM element:
let app = Component.div(
~children=[Component.text("Hello, World!")],
()
)
Component.mountById(app, "app")
Or use mount with a DOM element:
switch Document.getElementById("root") {
| Some(element) => Component.mount(app, element)
| None => Console.error("Root element not found")
}
Example: Counter Component
Here's a complete counter component using both syntaxes:
JSX Version
open Xote
type counterProps = {initialValue: int}
let counter = (props: counterProps) => {
let count = Signal.make(props.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>
{Component.textSignal(() =>
"Count: " ++ Int.toString(Signal.get(count))
)}
</h2>
<div class="controls">
<button onClick={decrement}>
{Component.text("-")}
</button>
<button onClick={increment}>
{Component.text("+")}
</button>
</div>
</div>
}
// Use the component
let app = counter({initialValue: 10})
Component.mountById(app, "app")
Function API Version
open Xote
let counter = (~initialValue=0, ()) => {
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)
}
Component.div(
~attrs=[Component.attr("class", "counter")],
~children=[
Component.h2(~children=[
Component.textSignal(() =>
"Count: " ++ Int.toString(Signal.get(count))
)
], ()),
Component.div(
~attrs=[Component.attr("class", "controls")],
~children=[
Component.button(
~events=[("click", decrement)],
~children=[Component.text("-")],
()
),
Component.button(
~events=[("click", increment)],
~children=[Component.text("+")],
()
),
],
()
),
],
()
)
}
// Use the component
let app = counter(~initialValue=10, ())
Component.mountById(app, "app")
Component Composition
Build complex UIs by composing smaller components:
JSX Version
type buttonProps = {
label: string,
onClick: Dom.event => unit,
}
let button = (props: buttonProps) => {
<button class="btn" onClick={props.onClick}>
{Component.text(props.label)}
</button>
}
let toolbar = () => {
<div class="toolbar">
{button({label: "Save", onClick: handleSave})}
{button({label: "Cancel", onClick: handleCancel})}
</div>
}
Function API Version
let button = (~label, ~onClick, ()) => {
Component.button(
~attrs=[Component.attr("class", "btn")],
~events=[("click", onClick)],
~children=[Component.text(label)],
()
)
}
let toolbar = () => {
Component.div(
~attrs=[Component.attr("class", "toolbar")],
~children=[
button(~label="Save", ~onClick=handleSave, ()),
button(~label="Cancel", ~onClick=handleCancel, ()),
],
()
)
}
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 parameters:
- JSX: Use record types for props
- Function API: Use labeled parameters
- Compose components: Build complex UIs from simple, reusable components
- Name components clearly:
- JSX: Use camelCase names (e.g.,
todoItem,userProfile) - Function API: Use camelCase or lowercase names
- JSX: Use camelCase names (e.g.,
- Choose the right list type:
- Use
listKeyedfor dynamic lists that can change - Use
listonly for simple, static lists
- Use
- Use
classnotclassName: In JSX, use theclassprop for CSS classes - Prefer JSX for new code: JSX syntax is more concise and familiar to most developers
Next Steps
- Try the Counter Demo to see basic components in action
- Explore the Todo List Demo for reactive lists and event handling
- Check out the Color Mixer Demo for complex reactive patterns
- View all Examples to see complete applications