Introduction
synstate-dom is a UI library that binds synstate observables directly to the DOM. You write JSX and pass observables wherever values go. Unlike Solid, it needs no compiler, and there is no virtual DOM.
Why synstate-dom
Section titled “Why synstate-dom”synstate-dom is the UI library for SynState. It is built to connect to SynState’s runtime as directly as a UI can: the graph you build with SynState’s operators ends in the DOM node bound to it, with no value converted into another library’s model on the way and no component re-run to read it.
- SynState’s speed reaches the screen. In SynState’s benchmark summary, SynState stays within 2.7× of the fastest library in every scenario: the only library measured in all of them whose worst case stays in single digits. synstate-dom puts nothing between that graph and the page.
- A simple API that is rarely slow. synstate-dom keeps one way to write each thing rather than adding forms that only win a row of a benchmark: a transformed value is
source$.pipe(map(fn))wherever it is bound. On the operations of js-framework-benchmark, its slowest operation takes 1.56× the fastest library’s time: behind Solid, Preact and Preact + Signals, and well ahead of React, whose slowest takes 3.79×. See Benchmark.
The model
Section titled “The model”-
A component is called once. It runs when it mounts, returns a description of the DOM, and is never called again. From then on, the only thing that changes is the DOM bound to the observables you gave it.
-
Updates go straight to the leaves. When a state changes, synstate propagates the change through its graph in one synchronous, glitch-free pass, and synstate-dom writes it directly into each bound attribute, property or text node. Nothing is diffed.
-
Dependencies are values. A component depends on the observables you pass it. So there is nothing to track while it runs, and no JSX expression has to be compiled into a lazy one, as Solid’s compiler does. See Without a compiler.
-
The types say what is reactive. Anywhere that takes data accepts a
Bindable<T>: an observable with an initial value or aconstant(...). A plain value is a type error, so you cannot pass a snapshot by mistake. See Bindable values.In Solid, a prop has the plain type
Twhether the caller passed a signal read or a fixed value. The types cannot tell a value that should update from one that should not, so they cannot reject the wrong one either. If you read a prop once in the component body, or destructure props, the updates silently stop, and only lint rules (solid/reactivityandsolid/no-destructureof eslint-plugin-solid) will point it out. In synstate-dom, whether something updates comes down to whether it is aBindable: you can see it in the signature, andtscchecks it.And since a prop holds the
Bindableitself, not a value read from it, props need no special handling: you can destructure them, give them defaults, forward the rest, or compute from them in the component body. The price is that a value derived from props that should keep updating is built as a bindable (withmapBindableorderiveBindable) rather than written as an expression in JSX. See Props are plain JavaScript. -
Scopes clean up after you. Each branch of
If/Matchand each row ofFormounts in a scope. Any derivation or subscription created while it mounts belongs to that scope and is disposed with it. See Lifecycle and scopes.
What it is not (yet)
Section titled “What it is not (yet)”- No server-side rendering or hydration.
- No context: pass what a subtree needs through props, or import the store.
- No re-running components, as Preact Signals or React do.
Where to go next
Section titled “Where to go next”- Installation sets up the JSX transform and renders a counter.
- Benchmark compares synstate-dom with Solid, Preact + Signals, Preact, React and VanJS on the operations of js-framework-benchmark, and Bundle size compares how much each adds to the same applications.