Skip to content

Installation

Install synstate-dom together with synstate, which your application imports to create its state. synstate is a peer dependency, so your application and synstate-dom share one instance:

Terminal window
npm install synstate-dom synstate

The other peer dependency, ts-data-forge, comes along as a dependency of synstate. Once your application imports it directly, for example for Optional in Control flow, add it yourself.

Point the JSX transform at synstate-dom in tsconfig.json. That is all you need: the transform is TypeScript’s own, so any bundler that reads jsxImportSource (Vite, esbuild) picks it up.

{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "synstate-dom",
},
}
import { createState, map } from 'synstate';
import { constant, render, type JSX } from 'synstate-dom';
const [count$, _setCount, { updateState: updateCount }] = createState(0);
// Called once. Only the text node bound to `count$` changes afterwards.
const Counter = (): JSX.Element => (
<div>
<p>{count$.pipe(map((n) => `Count: ${n}`))}</p>
<button
type={constant('button')}
onClick={() => {
updateCount((n) => n + 1);
}}
>
{constant('+1')}
</button>
</div>
);
const unmount = render(Counter, document.body);
// Later: disposes every subscription the app made and removes its nodes.
unmount();

render(App, container) takes the component itself, not <App />, and calls it once inside a root scope. It returns a function that unmounts the app: calling it disposes every subscription and derivation the app made and removes its nodes.

Text in JSX is a Bindable too, so you write the label as constant('+1') rather than +1. Bindable values explains the reason.