Radiant0.3.0-rc.10

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() and subscribe(...)

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/jsx owns TSX syntax, template creation, DOM mounting, hydration, and SSR serialization.
  • @ecopages/radiant owns 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.

EntrypointUse it for
@ecopages/jsxShared app code: JSX primitives, createRoot(...), hydration helpers, and types
@ecopages/jsx/clientBrowser-only entry files (same mounting surface as the root barrel)
@ecopages/jsx/serverNode or Bun SSR: renderToString(...), custom-element render hooks, and SSR scope helpers
@ecopages/jsx/jsx-runtimeAutomatic JSX runtime wiring (jsx, jsxs, Fragment)
@ecopages/jsx/jsx-dev-runtimeDevelopment runtime when the toolchain emits jsxDEV(...)

Install And Configure

npm install @ecopages/radiant @ecopages/jsx

@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:

  1. JSX produces a renderer-neutral template result.
  2. That template result can go to the DOM renderer or the SSR renderer.
  3. 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, and false render no child content
  • null and undefined remove normal attributes
  • false removes boolean attributes such as hidden or disabled
  • null removes event handlers by leaving no next listener to attach
  • undefined clears property bindings by writing undefined
/** @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