JSX Overview
@ecopages/jsx is the JSX authoring layer for the Radiant ecosystem.
Use it when you want:
- TSX syntax
- typed intrinsic HTML and SVG elements
- direct DOM mounting
- server rendering and hydration helpers
- fine-grained child bindings without a hook runtime
@ecopages/jsx also gives you:
- explicit DOM event bindings with
on:* - native listener escape hatches with
on-native:* - property bindings with
prop:* - direct child bindings from reactive sources that expose
get()andsubscribe(...)
Keep using @ecopages/radiant when you need custom-element classes, decorators, lifecycle, context, and host-managed reactivity.
Package Boundary
This is the key split:
@ecopages/jsxowns TSX syntax, template creation, DOM mounting, hydration, and SSR serialization.@ecopages/radiantowns component behavior and decides when a host should render.- Both the DOM and SSR flows consume the same template result shape.
Install And Configure
@ecopages/radiant depends on @ecopages/signals directly, so installing it brings signals automatically. @ecopages/jsx declares @ecopages/signals as a peer dependency; install signals explicitly only when using @ecopages/jsx standalone with signal-backed bindings such as mapSubscribable(...).
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@ecopages/jsx"
}
}Or enable it per file:
/** @jsxImportSource @ecopages/jsx */Mental Model
The shortest accurate model is:
- JSX produces a renderer-neutral template result.
- That template result can go to the DOM renderer or the SSR renderer.
- Fine-grained updates happen at bound child ranges, not through a hook scheduler.
Empty Values And Removal
Most code should use plain JavaScript values for empty output and removal semantics.
null,undefined, andfalserender no child contentnullandundefinedremove normal attributesfalseremoves boolean attributes such ashiddenordisablednullremoves event handlers by leaving no next listener to attachundefinedclears property bindings by writingundefined
/** @jsxImportSource @ecopages/jsx */
return (
<button
class={shouldResetClass ? null : 'toolbar-action'}
hidden={isVisible ? false : true}
on:click={isInteractive ? handleClick : null}
prop:payload={hasPayload ? payload : undefined}
>
{shouldShowLabel ? statusLabel : null}
</button>
);Removing a binding this way follows the normal renderer path. If the next render changes the template shape, the DOM renderer may replace the affected node rather than preserve the previous instance.
Derived Bindings
SubscribableJsxValue bindings can be projected in two ways:
Member access — for object keys, use inline in JSX:
<p>{this.$.config.label}</p>The runtime memoizes each key on the binding, so this is identity-stable across renders.
map — for transforms, record lookups, and non-property projections:
const themeLabel = boundPreference.map((preference) => THEME_CONFIG[preference].label);On Radiant hosts: this.$.field.map(...). Hoist map to a create-once host field. Do not call .map(...) inside render() — each call creates a new derived binding.
Derived bindings reuse the source getValue / subscribe contract and mount through the same reactive engines as any other binding.
Quick Start With RadiantElement
/** @jsxImportSource @ecopages/jsx */
import { RadiantElement, customElement, prop } from '@ecopages/radiant';
@customElement('counter-card')
export class CounterCard extends RadiantElement {
@prop({ type: Number, reflect: true, defaultValue: 0 }) count!: number;
private readonly increment = () => {
this.count += 1;
};
override render() {
return (
<section>
<h2>Count: {this.count}</h2>
<button type="button" on:click={this.increment}>
Increment
</button>
</section>
);
}
}Next Pages
- See JSX Event Handling for
on:*andon-native:*. - See JSX Rendering for
createRoot(...),renderToString(...), and hydration. - See RadiantElement for the host render lifecycle.