Introducing Xote 7.1

Hello everyone,

I’ve been working on a ReScript UI library for the past year, called Xote. I had the chance to present the library earlier this year during the ReScript Retreat 2026.

Recently I released a new version, the v7.1.0. This release is a milestone on Xote’s development, with a much more stable API and improved developer experience. Which is why I feel it finally deserves a proper introduction here in the community.

If you never tried Xote before or checked the library a long time ago, now would be a good time to do. Feedback is always welcome and appreciated.

What’s Xote?

Xote is a ReScript library for building UIs around fine-grained reactivity: signals, computeds, and effects instead of a virtual DOM. Views are plain functions, JSX compiles to native calls, and updates touch only the DOM nodes that actually changed.

It also ships a router, SSR, and hydration, so you can build a full app without stitching together a pile of additional packages. The goal is a small runtime, a very solid sound type system (thanks to ReScript), and a small overhead with no unnecessary abstraction layered on top.

Here’s a small example:

module GreetingAndCounter = {
  @xote.component
  let make = (~name) => {
    let count = Signal.make(0)                                 // State
    let duplicated = Computed.make(() => count > 2)   // Derived state

    Effect.run(() => {
      Console.log(Signal.get(Count))                           // Logs every `count` change
      None                                                     // Optional cleanup
    })
    
    <div>
       <h1>
        {`Hello, ${name}!`}
       </h1>
      <p>
        {`You clicked : ${Signal.get(count)}`}
      </p>
      <button onClick={() => Singal.update(c => c + 1)}>
        {"Click"}
      </button>
    </div>
  }
}


Xote.mountById(<GreetingAndCounter name="Xote" />, "#app")

What’s new in 7.1.0?

7.1.0 can be seen as a stabilization release after a better API consolidation from 7.0. But it also brings an important addition: @xote.component, a new PPX that changes how components get written day to day.

@xote.component

In 7.0, reactive values needed explicit primitives:

<div>
  <View.Text> "Count: " </View.Text>
  <View.Int> {count} </View.Int>
</div>

@xote.component reads your source and does this for you. Any {…} child in element position gets coerced at compile time: a signal read becomes a reactive leaf, a static value becomes static text, a node passes through:

@xote.component
let make = () => {
  let count = Signal.make(0)
  <div>
    {"Count: "}
    {Signal.get(count)}
  </div>
}

It also derives the props type from labeled arguments (just like @jsx.component), and wraps an if/switch in child position in tracked() automatically, so conditional branches re-render as a unit without you reaching for that API by hand.

There are some limitations in the PPX though: it can’t see through a call into another module.
For example, <p>{Store.count(store)} </p> compiles fine but never updates, because the Signal reference is in another module. Rather than fail silently, we have a runtime check that catches these scenarios and the compiler will tell you exactly how to fix it:

[Xote] Queue.res:42:19: this value reads a signal through a call
@xote.component cannot see. Wrap it in a thunk (`{() => ...}`), or use the reactive primitive.

Performance

This version also introduced some performance gains, mostly around list rendering. Here’s the latest benchmarks against React, Solid and Vue:

Xote React Vue Solid
Updating an existing list
Update every 10th row (ms) 6.3 11.7 7.4 5.4
Select a row (ms) 1.0 5.8 1.9 0.8
Swap two rows (ms) 7.3 64.8 7.8 6.3
Building and tearing down
Create 1,000 rows (ms) 88.7 62.1 59.2 52.5
Create 10,000 rows (ms) 918.9 947.3 705.8 660.4
Clear 10,000 rows (ms) 108.9 96.4 75.3 62.0
Startup, size, memory
Time to first render (ms) 24.4 41.8 28.5 22.8
App bundle, gzipped (KB) 8.4 59.9 24.9 4.7
Heap at 10,000 rows (MB) 40.5 20.2 19.0 12.3

Read more details about the benchmarks here.

Release Notes

Find the full release notes and changelogs at: Release v7.1.0 · brnrdog/xote · GitHub

Thank you!

2 Likes

Congrats on shipping a ReScript-native framework!

Is the JSX deliberately more relaxed than in the React PPX? Do you think we should relax it there as well? Personally I am against that in times of AI coding, but for review less noise is also nice.