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.
Entrypoints
Choose the narrowest import path for the environment you are writing in. Prefer subpaths in environment-specific modules so bundlers keep browser and server code separate.
| Entrypoint | Use it for |
|---|---|
@ecopages/jsx | Shared app code: JSX primitives, createRoot(...), hydration helpers, and types |
@ecopages/jsx/client | Browser-only entry files (same mounting surface as the root barrel) |
@ecopages/jsx/server | Node or Bun SSR: renderToString(...), custom-element render hooks, and SSR scope helpers |
@ecopages/jsx/jsx-runtime | Automatic JSX runtime wiring (jsx, jsxs, Fragment) |
@ecopages/jsx/jsx-dev-runtime | Development runtime when the toolchain emits jsxDEV(...) |
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.
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>
);
}
}Derived Bindings (Radiant Hosts)
On RadiantElement and RadiantController, this.$ exposes stable SubscribableJsxValue bindings for reactive members. Skip this section if you only use plain createRoot(...) mounting.
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 = this.$.preference.map((preference) => THEME_CONFIG[preference].label);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.
Next Pages
- See Authoring With JSX for intrinsics, components, fragments, and attribute normalization.
- See JSX Custom Element Types for type augmentation and
attr:*/prop:*defaults. - See JSX Event Handling for
on:*andon-native:*. - See JSX Client Rendering for
createRoot(...),hydrate(...), and container-level hydration. - See JSX SSR for
renderToString(...)and SSR scope helpers. - See Trusted Markup And Security for the escaping model and
unsafeHtml(...). - See RadiantElement for the host render lifecycle.