Radiant0.3.0-rc.2

JSX Client Rendering

This page is about the client entrypoints.

  • mount new DOM with createRoot(...).render(...)
  • hydrate existing SSR markup with hydrate(...) or createRoot(...).hydrate(...)

For server HTML generation, see JSX SSR.

Direct DOM Rendering

Use createRoot(...) when you want to mount JSX directly outside a Radiant custom-element host.

/** @jsxImportSource @ecopages/jsx */
 
import { createRoot } from '@ecopages/jsx';
 
function App() {
	return <p>Hello JSX</p>;
}
 
const container = document.querySelector('#app');
 
if (container instanceof HTMLElement) {
	createRoot(container).render(<App />);
}

The root API is intentionally small:

  • render(element) mounts or updates the target
  • hydrate(element) attaches bindings onto matching SSR output
  • unmount() disposes the mounted tree and clears the target

Hydrating Existing Markup

Hydration is the client attach step for markup that was produced with renderToString(..., { mode: 'hydrate' }).

/** @jsxImportSource @ecopages/jsx */
 
import { createRoot } from '@ecopages/jsx';
 
function App() {
	return <p>Hello JSX</p>;
}
 
const container = document.querySelector('#app');
 
if (container instanceof HTMLElement) {
	createRoot(container).hydrate(<App />);
}

You can also call hydrate(element, target) directly.

/** @jsxImportSource @ecopages/jsx */
 
import { hydrate } from '@ecopages/jsx';
 
function App() {
	return <p>Hello JSX</p>;
}
 
const container = document.querySelector('#app');
 
if (container instanceof HTMLElement) {
	hydrate(<App />, container);
}

If the target does not contain hydration markers, the runtime falls back to a normal client render.

For render-owning RadiantElement custom-element hosts, there is one extra gate: the explicit Radiant hydrator must also be installed. See Hydration.

Checking SSR Output

Use hasHydrationMarkers(...) when you need to detect whether a target contains SSR binding markers before attempting client attach behavior.

/** @jsxImportSource @ecopages/jsx */
 
import { createRoot, hasHydrationMarkers } from '@ecopages/jsx';
 
function App() {
	return <p>Hello JSX</p>;
}
 
const container = document.querySelector('#app');
 
if (container instanceof HTMLElement && hasHydrationMarkers(container)) {
	createRoot(container).hydrate(<App />);
}

Fine-Grained Child Updates

Child values can update without rerendering the parent tree when they expose get() and subscribe(...) or when you wrap an external source with createSubscribableJsxValue(...).

That makes this package useful both as a plain JSX runtime and as a renderer that can consume signal-like values directly.

Empty Values During Updates

Use normal JavaScript values for removals and empty output:

  • null, undefined, and false clear child content
  • null and undefined remove standard attributes
  • false removes boolean attributes
  • null removes event listeners
  • undefined clears property bindings
/** @jsxImportSource @ecopages/jsx */
 
import { createRoot } from '@ecopages/jsx';
 
const root = createRoot(document.querySelector('#app') as HTMLElement);
 
const handleTogglePin = () => {
	console.log('toggle pin');
};
 
const renderPinAction = (phase: 'idle' | 'saving') => (
	<button
		class={phase === 'saving' ? 'toolbar-action toolbar-action--saving' : null}
		aria={{ pressed: phase === 'saving' ? false : true }}
		data={{ phase }}
		disabled={phase === 'saving'}
		on:click={phase === 'saving' ? null : handleTogglePin}
		prop:payload={phase === 'saving' ? undefined : { phase }}
	>
		{phase === 'saving' ? 'Saving pin state...' : 'Pinned'}
	</button>
);
 
root.render(renderPinAction('idle'));
root.render(renderPinAction('saving'));

These updates go through the normal renderer path. If the template shape changes, the client renderer may replace the affected node rather than preserve the previously committed instance.

Runtime Output Contract

jsx() and jsxs() return a template result object with:

  • static string segments
  • dynamic values
  • a stable marker used by the Radiant renderers

The distinction between jsx() and jsxs() is important:

  • jsx() is emitted when the source has one logical child value
  • jsxs() is emitted when the source has multiple sibling children

Radiant uses that distinction to preserve child-slot structure from the automatic JSX transform.