Using JSX
JSX and S describe the same thing: connecting existing closures into a new state flow. JSX makes the composition read like a tree, especially when inputs contain other computations or callbacks.
We'll build a draft preview, then add text processing, customizable content, and a list.
Stative has its own JSX runtime. It does not depend on React or produce DOM. The examples use
.tsxfiles; see TypeScript setup at the end of this page.
Start with existing closures
Assume your project already has two building blocks: Clean trims text, and Label adds a prefix. Put them in base.tsx; we'll reuse them throughout the examples.
First, connect the draft state directly to Label. text is reactive; options is fixed configuration, passed unchanged through D():
This JSX is equivalent to S([Label, { text: draft, options: D(options) }]). JSX creates a descriptor; render creates the closure you can read, subscribe to, and destroy.
Turn a composition into a new closure
Now trim the text before adding the prefix. Replace text={draft} with text={<Clean text={draft} />} to connect the two computations.
To reuse this composition, wrap it in once, name it Preview, and save it as preview.tsx:
Callers only need to know what Preview accepts, regardless of how many closures it uses internally:
Preview manages the Clean and Label closures it creates. Destroying it also cleans up those children; the caller still owns the external draft state.
Use
onceor a class'srender()when returning JSX to compose child closures. A plain mapping function returns its result as a value; JSX inside that result is not resolved again.
Pass a render callback
Next, let a preview container decide how to present the result while the caller decides how to produce its content. Declare a callback returning IReadableClosure<string>. Here we name that input children. Save this as panel.tsx:
Inside Panel, children() always returns a readable closure. At the call site, the callback can return JSX directly: Stative turns it into a child closure.
When no extra computation is needed, the same callback can return an existing state or a fixed value:
children, renderItem, and create are all input names you choose. The receiving closure invokes the callback; writing JSX alone does not invoke it. Declare it as () => IReadableClosure<T>, or as a function taking one object argument and returning IReadableClosure<T>. That object argument may be optional. The receiving closure manages the generated children and cleans them up when it is destroyed.
Pass child-creation callbacks directly. Wrap ordinary formatting or event callbacks in
D(callback)to preserve the original function. If a callback needs hooks, put them in aonceclosure returned by the callback.
Let the list create items and the caller shape them
When the preview grows into multiple drafts, let DraftList manage the list and delegate each item's computation to renderItem. Save this as draft-list.tsx:
Reuse the same Preview as before:
useMapEach manages child computations by position, updating each item's state when the input changes. To preserve items by a business ID, use useMapKeyed.
For a fixed list, an ordinary array .map() is enough. Pass the resulting descriptor array to render:
Supported input forms
For once and classes, the input declaration tells you what the closure receives. These are the supported forms behind the examples above:
For example, children={() => <Preview ... />}, children={() => state}, and children={() => S(...)} are all factories. So is children={() => 'Fixed content'}. To pass a fixed string or number directly into a readable input, wrap it in ReactiveState.of(value).
For plain mapping functions, inputs are resolved to ordinary values first. States and JSX nested in arrays or objects are resolved recursively too. For example, a function declaring lines: string[] can receive lines={[<Clean text={draft} />, 'Fixed footer']}. D(value) preserves a value and its contents as they are.
To expose multiple outputs, pass an array or object to render, or return one from once or a class's render(). JSX tags can be plain functions, once functions, or classes, with no input or one object input.
Stative does not support HTML tags or fragments (
<>...</>).keyis an ordinary declared input, not a list-reconciliation mechanism. Use collection hooks for dynamic lists.
Configure TypeScript
Enable Stative JSX in your project and use the .tsx extension:
If your project also uses another JSX runtime, or you prefer to keep its global configuration, add
/** @jsxImportSource stative */at the very top of these.tsxfiles. Keep state-flow composition and UI code in separate files.