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 ashiddenordisablednullandundefinedremove event handlers by leaving no next listener to attach- an existing reactive property binding can be cleared by writing
undefined; a plain property part withundefinedis omitted during normalization
For ordinary attributes and event handlers, null and undefined both remove the current value or listener. They differ in template construction: null keeps the binding part so the current node can be updated in place, while undefined is dropped during normalization and can change the template shape, allowing the affected node to be replaced. The merged class/classes binding is an exception: it is omitted when no class tokens remain, including when its value is null. In TSX the types are stricter: event handlers and the shared global attributes (class, id, title, lang and the like) accept undefined but not null, so write undefined for those.
/** @jsxImportSource @ecopages/jsx */
return (
<button
class={shouldResetClass ? undefined : 'toolbar-action'}
hidden={isVisible ? false : true}
on:click={isInteractive ? handleClick : undefined}
prop:payload={hasPayload ? payload : undefined}
>
{shouldShowLabel ? statusLabel : null}
</button>
);Removing a binding this way follows the normal renderer path. For attributes and event handlers, undefined is dropped from the next template, so changing a previously defined binding to undefined changes the template shape and the DOM renderer may replace the affected node rather than preserve the previous instance.
Child content is different: a child expression keeps its place in the template whatever it holds. Showing, hiding or emptying a child, including a mapped list or an item in a fragment, patches that child only, and the element around it and its siblings keep their nodes. A mapped list such as rows.map(...) keeps its own range inside its parent, so its keyed items keep their nodes when an unkeyed item beside it comes and goes; unkeyed items are matched by position. Keys match within one list only: key the groups themselves, for example with a keyed Fragment, when items move between groups or a group is inserted before another. A key that appears twice in one list renders once, at its first position.
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 class-based props, type augmentation as a tag-name fallback, 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.