Skip to content

React Comparison

How Xote differs from React in rendering, effects, routing, SSR, and team tradeoffs.

At a Glance

Overview

AspectReactXote
Update modelRe-render component trees, then diffUpdate the specific reactive consumers directly
StateuseState, useReducer, external storesSignal, Computed, Effect
EffectsuseEffect with explicit dependency arraysEffect.run with tracked dependencies
RoutingThird-party packagesBuilt in
SSRMature ecosystem and frameworksBuilt-in primitives for SSR, hydration, and state transfer
LanguageJavaScript / TypeScriptReScript

React and Xote solve many of the same problems, but they optimize for different tradeoffs. React optimizes for ecosystem reach and framework maturity. Xote optimizes for a smaller runtime, explicit fine-grained reactivity, and a tighter built-in surface.

Runtime Model

Reactivity Model

React updates by re-running component functions and diffing the next virtual tree against the previous one. That model is flexible and well understood, but it means the render pass is the default unit of work.

Xote updates at the signal consumer level. When a signal changes, only the effects, computeds, or reactive DOM bindings that read that signal need to run again. The component function itself usually does not.

1import { useState } from "react";
2
3function Counter() {
4 const [count, setCount] = useState(0);
5
6 return (
7 <div>
8 <h1>Count: {count}</h1>
9 <button onClick={() => setCount(c => c + 1)}>Increment</button>
10 </div>
11 );
12}
1open Xote
2
3let counter = () => {
4 let count = Signal.make(0)
5
6 <div>
7 <h1>
8 <View.Text> "Count: " </View.Text>
9 <View.Int> {count} </View.Int>
10 </h1>
11 <button onClick={_ => Signal.update(count, n => n + 1)}>
12 <View.Text> "Increment" </View.Text>
13 </button>
14 </div>
15}

Effects and Derived State

React's useEffect and useMemo depend on manually maintained dependency arrays. That is workable, but stale or over-broad dependency lists are a common source of bugs and noise.

Xote tracks dependencies automatically. Effect.run subscribes to the signals it reads, and Computed.make derives values from the signals it reads.

1useEffect(() => {
2 document.title = `Count: ${count}`;
3}, [count]);
1Effect.run(() => {
2 document.title = `Count: ${Signal.get(count)->Int.toString}`
3 None
4})
5
6let doubled = Computed.make(() => Signal.get(count) * 2)

The tradeoff is that React's hook model is familiar to more teams and supported by more tooling, while Xote's model is smaller and more explicit once you adopt signals.

Component Lifecycle

React components re-run whenever their state or props change. That is why hooks exist: they preserve values across renders and enforce ordering rules.

Xote components usually run once. Signals, computeds, and effects are ordinary values created during that initial execution. Cleanup is handled by effect cleanups and the owner system that disposes reactive resources when DOM nodes are removed.

List Rendering

React uses keys during virtual DOM reconciliation. Xote uses View.For in JSX; passing by enables keyed reconciliation through Xote's DOM anchors.

1function TodoList({ todos }) {
2 return (
3 <ul>
4 {todos.map(todo => (
5 <li key={todo.id}>{todo.text}</li>
6 ))}
7 </ul>
8 );
9}
1let todoList = () => {
2 let todos = Signal.make([{id: "1", text: "Buy milk"}])
3
4 <ul>
5 <View.For
6 each={MaybeSignal.reactive(todos)}
7 by={todo => todo.id}
8 render={todo => <li> <View.Text> {todo.text} </View.Text> </li>}
9 />
10 </ul>
11}

In practice, both can preserve item identity. The difference is mostly where the work happens: inside a general-purpose renderer in React, or through a dedicated keyed-list primitive in Xote.

Platform Surface

Server-Side Rendering

React has the stronger SSR ecosystem. Frameworks like Next.js and Remix add routing, data loading, streaming, server actions, and deployment integrations on top of the core renderer.

Xote gives you lower-level primitives directly: SSR.renderToString, SSR.renderDocument, SSRState, and Hydration. That is enough for custom SSR pipelines, but it is intentionally not a batteries-included application framework.

1let html = SSR.renderDocument(
2 ~scripts=["/client.js"],
3 ~stateScript=SSRState.generateScript(),
4 app,
5)
6
7Hydration.hydrateById(app, "root")

Routing

React relies on external routers such as React Router or TanStack Router. That is not a weakness by itself, but it does mean routing decisions also become ecosystem decisions.

Xote includes a router in the main library. If you want pattern matching, links, imperative navigation, and SSR-aware initialization without another dependency, that is a meaningful simplification.

Runtime Footprint

React's runtime is larger because it carries a general rendering engine and is often paired with more packages. Xote stays smaller because the reactive graph and direct DOM updates remove the need for a general virtual DOM reconciliation path during normal updates.

Bundle size should not be the only decision criterion, but it matters for widgets, embedded apps, and performance-sensitive pages.

Benchmarks

The repository ships a keyed-list benchmark that runs the same table application in Xote, React, Vue, and SolidJS. Every implementation renders identical DOM from the same generated data, written the way each library recommends: React keeps an immutable row array in useState, Xote gives each row its own signal.

Median of 15 iterations, Chromium 141 on a 4-core Xeon. Lower is better. These are ratios from one machine, not absolute performance claims, and re-running the same code moves individual ratios by 10-25%.

OperationXoteReact
Create 1,000 rows88.7 ms62.1 ms
Replace 1,000 rows93.8 ms65.6 ms
Update every 10th row6.3 ms11.7 ms
Select a row1.0 ms5.8 ms
Swap two rows7.3 ms64.8 ms
Remove a row6.2 ms10.6 ms
Create 10,000 rows919 ms947 ms
Clear 10,000 rows109 ms96 ms
Time to first render24.4 ms41.8 ms
App bundle, gzipped8.4 KB59.9 KB

The results follow the architecture. Scattered updates and selection are where fine-grained reactivity pays: Xote writes only the text nodes that changed, while React re-renders the list and diffs it, so Xote runs about 2x faster on the update and several times faster on selection. Building large lists goes the other way, because React's element creation path is more optimized than Xote's node-by-node construction.

Clearing a large list used to be Xote's weakest result by some distance. Removal still walks the owner tree to dispose per-node effects, which a virtual DOM renderer does not have to do, but that walk no longer copies each node's children into a throwaway array on the way down, and elements that own nothing no longer carry a scope to dispose at all. Xote is now within about ten percent of React on it, rather than the seventy percent behind it used to be.

Swapping two rows is where the architectures separate. Xote's keyed reconciler leaves the rows that are already in the right relative order alone and moves only the two that are not, so the swap costs two insertBefore calls — the same as Vue and SolidJS. React's reconciler still walks the list and reinserts everything past the first mismatch, which is why it spends about ten times as long on the same operation.

Type Safety

React with TypeScript gives strong ergonomics and wide adoption, but the type system is still optional and structurally typed.

Xote inherits ReScript's sounder model. Pattern matching, option, and exhaustiveness checks reduce a class of runtime mistakes that TypeScript projects still need discipline to avoid.

Ecosystem

React is the safer choice if your project depends on third-party UI kits, data tooling, or hiring from a very large pool.

Xote is the better fit when you want to own the stack, keep runtime dependencies minimal, and work from a smaller but more integrated API.

Choosing Between Them

When to Choose React

  • Reach for React when ecosystem depth is a hard requirement.
  • Reach for React when the team is already fluent in React and TypeScript.
  • Reach for React when third-party UI kits or integrations are central to the product.
  • Reach for React when React Native is part of the broader platform story.

When to Choose Xote

  • Reach for Xote when you want fine-grained updates without a virtual DOM render cycle.
  • Reach for Xote when built-in routing and SSR primitives reduce project overhead.
  • Reach for Xote when ReScript's type model is part of the value proposition.
  • Reach for Xote when the UI is focused enough that a smaller ecosystem is a benefit, not a cost.

Migration Considerations

React developers usually adapt to Xote fastest when they stop looking for hook equivalents and instead map responsibilities directly:

  1. useState becomes Signal.make
  2. useMemo becomes Computed.make
  3. useEffect becomes Effect.run
  4. keyed .map() rendering becomes View.For with by when identity matters

The conceptual shift is from re-rendered components to persistent reactive values.

Further Reading