--- title: "JSX Client Rendering" description: "Client-side DOM rendering and hydration entrypoints in @ecopages/jsx." group: JSX order: 5 --- # JSX Client Rendering This page is about the client entrypoints. Import from `@ecopages/jsx` or `@ecopages/jsx/client`; keep `@ecopages/jsx/server` in server-only modules. - mount new DOM with `createRoot(...).render(...)` - hydrate existing SSR markup with `hydrate(...)` or `createRoot(...).hydrate(...)` For server HTML generation, see [JSX SSR](/docs/ssr/jsx-ssr). ## Direct DOM Rendering Use `createRoot(...)` when you want to mount JSX directly outside a Radiant custom-element host. ```tsx /** @jsxImportSource @ecopages/jsx */ import { createRoot } from '@ecopages/jsx'; function App() { return

Hello JSX

; } const container = document.querySelector('#app'); if (container instanceof HTMLElement) { createRoot(container).render(); } ``` 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' })`. ```tsx /** @jsxImportSource @ecopages/jsx */ import { createRoot } from '@ecopages/jsx'; function App() { return

Hello JSX

; } const container = document.querySelector('#app'); if (container instanceof HTMLElement) { createRoot(container).hydrate(); } ``` You can also call `hydrate(element, target)` directly. ```tsx /** @jsxImportSource @ecopages/jsx */ import { hydrate } from '@ecopages/jsx'; function App() { return

Hello JSX

; } const container = document.querySelector('#app'); if (container instanceof HTMLElement) { hydrate(, 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](/docs/ssr/hydration). ## Checking SSR Output Use `hasHydrationMarkers(...)` when you need to detect whether a target contains SSR binding markers before attempting client attach behavior. ```tsx /** @jsxImportSource @ecopages/jsx */ import { createRoot, hasHydrationMarkers } from '@ecopages/jsx'; function App() { return

Hello JSX

; } const container = document.querySelector('#app'); if (container instanceof HTMLElement && hasHydrationMarkers(container)) { createRoot(container).hydrate(); } ``` ## 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. ```tsx /** @jsxImportSource @ecopages/jsx */ import { createRoot, createSubscribableJsxValue } from '@ecopages/jsx'; let count = 0; const subscribers = new Set<(value: number) => void>(); const boundCount = createSubscribableJsxValue({ getValue: () => count, subscribe: (notify) => { subscribers.add(notify); return () => { subscribers.delete(notify); }; }, }); const root = createRoot(document.querySelector('#app') as HTMLElement); root.render(

Count: {boundCount}

); count += 1; for (const subscriber of subscribers) { subscriber(count); } ``` Pass signal-like children directly when they already expose `get()` and `subscribe(...)`. Use `createSubscribableJsxValue(...)` when the source has its own update notifications but does not match that shape. ## Empty Values During Updates See [JSX Overview — Empty Values And Removal](/docs/jsx/overview#empty-values-and-removal) for the full rules. The sign-in button below applies them across two client updates: ```tsx /** @jsxImportSource @ecopages/jsx */ import { createRoot } from '@ecopages/jsx'; const root = createRoot(document.querySelector('#app') as HTMLElement); type SignInState = { username: string; busy: boolean; }; const signIn = () => console.log('sign in'); const renderSignInButton = (state: SignInState) => ( ); root.render(renderSignInButton({ username: '', busy: false })); root.render(renderSignInButton({ username: 'ada', busy: true })); ``` How each removal rule applies between those two renders: - `disabled={...}` — evaluates to `true` while the username is empty or a request is in flight; when it evaluates to `false`, the boolean attribute is removed - `title={... ? '...' : null}` — `null` removes the attribute once a username exists - `on:click={... ? null : signIn}` — `null` detaches the handler while a sign-in request is in flight - child text swaps between the two states as normal content The template shape is the same in both renders, so the renderer preserves the committed button node and only patches bindings. That is the general rule: if a later render changes the template shape, the client renderer may replace the affected node instead of preserving the previous 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.