--- 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(Hello JSX
; } const container = document.querySelector('#app'); if (container instanceof HTMLElement) { createRoot(container).hydrate(Hello JSX
; } const container = document.querySelector('#app'); if (container instanceof HTMLElement) { hydrate(Hello JSX
; } const container = document.querySelector('#app'); if (container instanceof HTMLElement && hasHydrationMarkers(container)) { createRoot(container).hydrate(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.