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 {Node.signalText(() => "Count: " ++ Int.toString(Signal.get(count)))}
9 </h1>
10 <button onClick={_ => Signal.update(count, n => n + 1)}>
11 {Node.text("Increment")}
12 </button>
13 </div>
14}

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: " ++ Int.toString(Signal.get(count))
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 Node.keyedList, which works directly against DOM anchors and explicit keys.

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 {Node.keyedList(
6 todos,
7 todo => todo.id,
8 todo => <li> {Node.text(todo.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.

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 Node.keyedList when identity matters

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

Further Reading