Technical Overview
A lower-level view of Xote's modules, runtime behavior, and rendering model.
This page explains how Xote is put together at a module and runtime level. It is meant for readers who already know the public API and want the internal model behind it.
Scope: This is architecture-level guidance, not a substitute for the API docs.
System Shape
Architecture Overview
Module Structure
The public surface is intentionally small. Xote re-exports reactive primitives and layers UI-focused modules on top.
Signal,Computed,Effect- state, derived state, and side effectsNodeandHtml- node constructors, attributes, mounting, and HTML helpersXoteJSX- generic JSX v4 integrationRouterandRoute- navigation and route matchingSSR,SSRState,Hydration, andSSRContext- server rendering and client resume
Source files stay as bare module names in src/. ReScript's namespace: true setting scopes them under Xote for consumers.
Runtime Model
Reactivity Model
Xote delegates reactivity to rescript-signals. The important runtime properties are:
- Tracked reads: Signal.get subscribes the active observer
- Synchronous scheduling: updates flush immediately unless wrapped in Signal.batch
- Lazy computeds: upstream changes mark them dirty, but recomputation happens on read
- Equality checks on write: signals notify only when the new value is considered different
Component Rendering
Xote does not rely on a general virtual DOM diff for updates. Components produce node structures once, and reactive nodes handle fine-grained updates after mounting.
The public node variants cover the main cases: text, elements, fragments, signal-backed text, signal-backed fragments, lazy components, and keyed lists.
1Node.text : string => node2Node.signalText : (unit => string) => node3Node.fragment : array<node> => node4Node.signalFragment : Signal.t<array<node>> => node5Node.list : (Signal.t<array<'a>>, 'a => node) => node6Node.keyedList : (Signal.t<array<'a>>, 'a => string, 'a => node) => nodeRouter Architecture
The router stores its state in a global singleton keyed with Symbol.for. That keeps routing shared even if more than one Xote bundle ends up on the page.
At the public layer, the router is just a signal-driven location source plus helpers for matching and navigation.
SSR and Hydration
Server rendering serializes the component tree to HTML and inserts comment markers around reactive boundaries. Hydration walks that DOM, finds the markers, and reattaches reactive behavior instead of rebuilding the tree from scratch.
SSRState is separate from HTML rendering. That split keeps state transfer explicit and codec-driven instead of hiding it behind a framework convention.
Execution Characteristics
- Component functions are cheap to reason about: most of the time they run once
- Reactive work is localized: only consumers of changed signals update
- Owner-based cleanup prevents leaks: DOM removal disposes associated reactive resources
- Batching is explicit: coordinated writes are opt-in, not automatic
Reference Map
API Summary
Reactive Primitives
1Signal.make : ('a, ~name: option<string>=?, ~equals: option<('a, 'a) => bool>=?) => Signal.t<'a>2Signal.get : Signal.t<'a> => 'a3Signal.peek : Signal.t<'a> => 'a4Signal.set : (Signal.t<'a>, 'a) => unit5Signal.update : (Signal.t<'a>, 'a => 'a) => unit6Signal.batch : (unit => 'a) => 'a7Computed.make : (unit => 'a, ~name: option<string>=?, ~equals: option<('a, 'a) => bool>=?) => Signal.t<'a>8Effect.run : (unit => option<unit => unit>, ~name: option<string>=?) => unitComponent Helpers
1Node.attr : (string, string) => (string, Node.attrValue)2Node.signalAttr : (string, Signal.t<string>) => (string, Node.attrValue)3Node.computedAttr : (string, unit => string) => (string, Node.attrValue)4Node.mountById : (Node.node, string) => unitRouter Helpers
1Router.init : (~basePath: string=?, unit) => unit2Router.initSSR : (~basePath: string=?, ~pathname: string, ~search: string=?, ~hash: string=?, unit) => unit3Router.location : unit => Signal.t<{pathname: string, search: string, hash: string}>4Router.push : (string, ~search: string=?, ~hash: string=?, unit) => unit5Router.routes : array<routeConfig> => Node.nodeWorking Style
Best Practices
- Model derived values as computeds so write paths stay smaller and easier to trust.
- Keep the public explanation aligned with the real module boundaries: signals, nodes, router, and SSR each own a distinct concern.
- Treat
SSRStateas explicit infrastructure. Hidden state transfer is harder to debug.
Next Steps
- Go back to the Core Modules guides for the day-to-day API surface.
- Use the Signals API page as the quick reference while reading the architecture back into the code.