Xote is a lightweight UI library for ReScript that combines fine-grained reactivity with a minimal component system. Built on rescript-signals, it provides declarative components, JSX support, and signal-based routing for building reactive web applications.
- Reactive Components: Declarative UI building with JSX support and direct DOM updates
- Signal-based Reactivity: Powered by rescript-signals for automatic dependency tracking
- Fine-grained Updates: Direct DOM manipulation without virtual DOM diffing
- Signal-based Router: SPA navigation with pattern matching and dynamic parameters
- Server-Side Rendering: SSR with hydration and automatic state transfer
- Lightweight: Minimal runtime footprint
- Type-safe: Full ReScript type safety throughout
npm install xote
# or
yarn add xote
# or
pnpm add xoteThen, add it to your ReScript project’s dependencies in rescript.json:
{
"bs-dependencies": ["xote"]
}Xote uses rescript-signals for reactive primitives (Signal, Computed, Effect), and it adds:
- Component System: A minimal but powerful component model with JSX support for declarative UI
- Direct DOM Updates: Fine-grained reactivity that updates DOM elements directly, no virtual DOM
- Signal-based Router: Client-side routing with pattern matching and reactive location state
- Reactive Attributes: Support for static, signal-based, and computed attributes on elements
- Automatic Cleanup: Effect disposal and memory management built into the component lifecycle
Xote focuses on clarity, control, and performance. The goal is to offer precise, fine-grained updates and predictable behavior with minimal abstractions, while leveraging the robust type system from ReScript.
open Xote
module App = {
let make = () => {
// Create reactive state
let count = Signal.make(0)
// Event handler
let increment = (_evt: Dom.event) => Signal.update(count, n => n + 1)
// Build the UI with JSX
<div>
<h1> {Component.text("Counter")} </h1>
<p>
{Component.textSignal(() => `Count: ${Signal.get(count)->Int.toString}`)}
</p>
<button onClick={increment}>
{Component.text("Increment")}
</button>
</div>
}
}
// Mount to the DOM
Component.mountById(<App />, "app")- Signal: Reactive state container -
Signal.make(value) - Computed: Derived reactive value that updates automatically -
Computed.make(() => ...) - Effect: Side-effect functions that re-run when dependencies change -
Effect.run(() => ...)
All reactive primitives feature automatic dependency tracking - no manual subscriptions needed.
- Component: Declarative UI builder with JSX syntax and function-based APIs
- Router: Signal-based navigation for SPAs with pattern matching and dynamic routes
- JSX syntax: Use HTML tags like
<div>,<button>,<input> - Props: Standard HTML attributes like
class,id,style,value,placeholder - Event handlers:
onClick,onInput,onChange,onSubmit, etc. - Reactive content: Wrap reactive text with
Component.textSignal(() => ...) - Component functions: Define reusable components as functions that return JSX
- Initialization: Call
Router.init()once at app start - Imperative navigation: Use
Router.push()andRouter.replace()to navigate programmatically - Declarative routing: Define routes with
Router.routes()and render components based on URL patterns - Dynamic parameters: Extract URL parameters using
:paramsyntax (e.g.,/users/:id) - Navigation links: Use
Router.link()for SPA navigation without page reload - Reactive location: Access current route via
Router.locationsignal
Xote supports server-side rendering with hydration. The same component code runs on both server and client, with the server rendering HTML and the client attaching reactivity to the existing DOM.
Shared component (App.res):
open Xote
let makeAppState = () => {
// SSRState.make creates a signal that syncs between server and client
let count = SSRState.make("count", 0, SSRState.Codec.int)
let items = SSRState.make("items", ["Apple", "Banana"], SSRState.Codec.array(SSRState.Codec.string))
(count, items)
}
let app = (count, items) => () => {
<div>
<p> {Component.textSignal(() => `Count: ${Signal.get(count)->Int.toString}`)} </p>
<button onClick={_ => Signal.update(count, n => n + 1)}>
{Component.text("+")}
</button>
</div>
}Server entry (server.res):
open Xote
let (count, items) = App.makeAppState()
let appComponent = App.app(count, items)
let html = SSR.renderDocument(
~head="<title>My App</title>",
~scripts=["./client.res.mjs"],
~stateScript=SSRState.generateScript(),
appComponent,
)
Console.log(html)Client entry (client.res):
open Xote
let (count, items) = App.makeAppState()
let appComponent = App.app(count, items)
Hydration.hydrateById(appComponent, "root")# Generate HTML
node server.res.mjs > index.html
# Serve with Vite (or any static server)
npx viteSSR.renderToString: Render component to HTML stringSSR.renderDocument: Render full HTML document with head, scripts, stylesSSRState.make: Create signals that automatically sync between server and clientSSRState.generateScript: Generate<script>tag with serialized stateHydration.hydrate: Attach reactivity to server-rendered DOMSSRContext.isServer/isClient: Environment detection for conditional logic
SSRState.Codec.int
SSRState.Codec.float
SSRState.Codec.string
SSRState.Codec.bool
SSRState.Codec.array(itemCodec)
SSRState.Codec.option(itemCodec)
SSRState.Codec.dict(valueCodec)
SSRState.Codec.tuple2(codec1, codec2)
SSRState.Codec.tuple3(codec1, codec2, codec3)
SSRState.Codec.make(~encode, ~decode) // Custom codecCheck some examples of applications built with Xote at https://brnrdog.github.io/xote/demos/.
To run the example demos locally:
- Clone the repository:
git clone https://github.com/brnrdog/xote.git
cd xote- Install dependencies:
npm install- Compile ReScript and start the dev server:
npm run res:dev # In one terminal (watches ReScript files)
npm run dev # In another terminal (starts Vite dev server)- Open your browser and navigate to
http://localhost:5173
The demo app includes a navigation menu to explore all examples interactively.
Comprehensive documentation with live embedded demos is available at:
https://brnrdog.github.io/xote/
To build and preview the documentation site:
npm run docs:startThis will build the demos and start the documentation server at http://localhost:3000.
LGPL v3