---
title: "Client DOM and Hooks"
description: "Hono provides a lightweight, React-compatible client DOM rendering and reconciliation engine via hono/jsx/dom that manages browser mounting, state management, and updates. It exposes mounting APIs ..."
last_updated: "2026-10-12T08:14:12.740617+00:00"
canonical_url: "https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/jsx-and-ui/client-dom-and-hooks"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [src/jsx/dom/render.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts)
- [src/jsx/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/components.ts)
- [src/jsx/hooks/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts)
- [src/jsx/dom/client.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts)
- [src/jsx/streaming.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts)
- [src/jsx/base.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/base.ts)
- [src/jsx/dom/intrinsic-element/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/intrinsic-element/components.ts)
- [src/jsx/jsx-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/jsx-runtime.ts)
- [src/jsx/intrinsic-element/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/intrinsic-element/components.ts)
- [src/jsx/dom/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/index.ts)
- [src/middleware/jsx-renderer/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/middleware/jsx-renderer/index.ts)
- [src/jsx/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/index.ts)
- [src/jsx/dom/hooks/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/hooks/index.ts)
- [src/jsx/dom/jsx-dev-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-dev-runtime.ts)
- [src/jsx/jsx-dev-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/jsx-dev-runtime.ts)
- [src/jsx/dom/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts)
- [src/jsx/dom/css.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/css.ts)
- [src/jsx/context.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/context.ts)
- [src/jsx/dom/server.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/server.ts)
- [src/helper/css/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/helper/css/index.ts)
- [src/jsx/dom/jsx-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-runtime.ts)
- [src/jsx/dom/context.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/context.ts)
- [src/utils/html.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/utils/html.ts)
- [src/jsx/children.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/children.ts)
- [src/jsx/constants.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/constants.ts)
- [src/jsx/dom/utils.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/utils.ts)
- [src/jsx/intrinsic-elements.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/intrinsic-elements.ts)
- [src/jsx/types.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/types.ts)
- [runtime-tests/deno-jsx/deno.react-jsx.json](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/runtime-tests/deno-jsx/deno.react-jsx.json)
</details>

## Overview

Hono provides a lightweight, React-compatible client DOM rendering and reconciliation engine via `hono/jsx/dom` that manages browser mounting, state management, and updates. It exposes mounting APIs such as `createRoot` and `hydrateRoot` to attach JSX element trees to document containers and orchestrate tree reconciliation. Core state and lifecycle primitives like `useState`, `useEffect`, and `useMemo` handle component reactivity and effect execution, while advanced hooks including `useTransition`, `useDeferredValue`, and `useSyncExternalStore` coordinate concurrency and external data stores. Additionally, browser context propagation, error boundaries, client-specific intrinsic elements, and specialized hooks support interactive form handling and metadata management.

Sources: [src/jsx/dom/client.ts:23-84](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts#L23-L84), [src/jsx/dom/render.ts:497-516](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L497-L516), [src/jsx/hooks/index.ts:119-176](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L119-L176), [src/jsx/dom/hooks/index.ts:41-90](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/hooks/index.ts#L41-L90)

## Client Entry Points and Roots

### Public Mounting APIs and Root Management

Hono exposes browser client mounting functions through `src/jsx/dom/client.ts`, providing compatibility with `react-dom/client`. The core API centers on `createRoot`, which accepts an `HTMLElement` or `DocumentFragment` render target and an optional `RootOptions` record. If any options are supplied, a development warning is emitted via `console.warn('createRoot options are not supported yet')`.

```typescript
export const createRoot = (
  element: HTMLElement | DocumentFragment,
  options: RootOptions = {}
): Root => {
  let setJsxNode:
    | undefined // initial state
    | ((jsxNode: unknown) => void) // rendered
    | null = // unmounted
    undefined

  if (Object.keys(options).length > 0) {
    console.warn('createRoot options are not supported yet')
  }

  return {
    render(jsxNode: unknown) {
      if (setJsxNode === null) {
        // unmounted
        throw new Error('Cannot update an unmounted root')
      }
      if (setJsxNode) {
        // rendered
        setJsxNode(jsxNode)
      } else {
        renderNode(
          buildNode({
            tag: () => {
              const [_jsxNode, _setJsxNode] = useState(jsxNode)
              setJsxNode = _setJsxNode
              return _jsxNode
            },
            props: {},
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
          } as any) as NodeObject,
          element
        )
      }
    },
    unmount() {
      setJsxNode?.(null)
      setJsxNode = null
    },
  }
}
```
Sources: [src/jsx/dom/client.ts:23-65](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts#L23-L65)

The returned `Root` object exposes two primary methods: `render(children: Child)` and `unmount()`. The internal lifecycle state tracked by `setJsxNode` transitions through three phases:
* `undefined`: Initial unrendered state, triggering node construction and DOM insertion via `renderNode` and `buildNode`.
* Function: Active rendered state where subsequent calls to `root.render()` invoke `setJsxNode(jsxNode)` to schedule updates.
* `null`: Unmounted state, where invoking `render()` throws an error (`Cannot update an unmounted root`), and calling `unmount()` sets the node value to `null`.

```typescript
export const hydrateRoot = (
  element: HTMLElement | DocumentFragment,
  reactNode: Child,
  options: RootOptions = {}
): Root => {
  const root = createRoot(element, options)
  root.render(reactNode)
  return root
}
```
Sources: [src/jsx/dom/client.ts:76-84](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts#L76-L84)

> [!NOTE]
> In `hono/jsx/dom`, `hydrateRoot` is functionally equivalent to `createRoot` followed immediately by `root.render(reactNode)`.

Sources: [src/jsx/dom/client.ts:68-70](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts#L68-L70), [src/jsx/dom/client.ts:76-84](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts#L76-L84)

### JSX Runtime and Compilation Setup

The package provides two JSX runtimes under `src/jsx/dom/jsx-runtime.ts` and `src/jsx/dom/jsx-dev-runtime.ts`. Both modules export `jsxDEV` as the underlying factory function for `jsx`, `jsxs`, and development fragments.

```typescript
export const jsxDEV = (tag: string | Function, props: Props, key?: string): JSXNode => {
  if (typeof tag === 'string' && intrinsicElementTags[tag as keyof typeof intrinsicElementTags]) {
    tag = intrinsicElementTags[tag as keyof typeof intrinsicElementTags]
  }
  return {
    tag,
    type: tag,
    props,
    key,
    ref: props.ref,
  } as JSXNode
}

export const Fragment = (props: Record<string, unknown>): JSXNode => jsxDEV('', props, undefined)
```
Sources: [src/jsx/dom/jsx-dev-runtime.ts:10-22](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-dev-runtime.ts#L10-L22)

When `jsxDEV` processes an intrinsic element tag supplied as a string, it checks against `intrinsicElementTags`. If a matching component definition exists in `src/jsx/dom/intrinsic-element/components`, the string tag is resolved directly to that component function. The resulting `JSXNode` bundles the resolved tag, `type`, `props`, optional `key`, and `ref` extracted from `props.ref`.

Sources: [src/jsx/dom/jsx-dev-runtime.ts:8-20](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-dev-runtime.ts#L8-L20), [src/jsx/dom/jsx-runtime.ts:1-8](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-runtime.ts#L1-L8)

## DOM Rendering and Node Reconciliation

### Reconciliation Engine and Node Construction

The reconciliation engine manages DOM trees through `applyNodeObject`, which coordinates node creation, updates, and removals by evaluating virtual node objects against a container element. It processes next children arrays and cleanup lists, mapping virtual representations to live DOM nodes.

```typescript
const applyNodeObject = (node: NodeObject, container: Container, isNew: boolean): void => {
  const next: Node[] = []
  const remove: Node[] = []
  const callbacks: EffectData[] = []
  getNextChildren(node, container, next, remove, callbacks)
  remove.forEach(removeNode)
  // ...
}
```
Sources: [src/jsx/dom/render.ts:387-392](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L387-L392)

When constructing or updating DOM elements, `applyNodeObject` verifies whether nodes can be reused or if text nodes and element structures need instantiation. String nodes update their `textContent` when flagged as dirty (`child.d`), or get created via `document.createTextNode`. Elements utilize `document.createElement` or `document.createElementNS` for namespace-aware tags such as SVG and MathML, retrieving namespaces via `nameSpaceMap`.

| Namespace Key | URI Value |
| :--- | :--- |
| `svg` | `2000/svg` |
| `math` | `1998/Math/MathML` |

Sources: [src/jsx/dom/render.ts:24-27](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L24-L27), [src/jsx/dom/render.ts:420-435](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L420-L435)

> [!NOTE]
> Event aliases are normalized during property application; `Change` maps to `Input` and `DoubleClick` maps to `DblClick`.

Sources: [src/jsx/dom/render.ts:19-22](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L19-L22)

### Child Indexing and DOM Mutation

Child node positioning relies on `findChildNodeIndex`, which scans container child node collections to locate matching reference nodes. During inserts and updates, offsets are resolved relative to existing child nodes or insertion bounds, placing nodes via `container.appendChild` or `container.insertBefore`.

```typescript
const findChildNodeIndex = (
  childNodes: NodeListOf<ChildNode>,
  child: ChildNode | undefined
): number | undefined => {
  if (!child) {
    return
  }

  for (let i = 0, len = childNodes.length; i < len; i++) {
    if (childNodes[i] === child) {
      return i
    }
  }

  return
}
```
Sources: [src/jsx/dom/render.ts:369-384](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L369-L384)

Portals use the internal marker `_hp` (`HONO_PORTAL_ELEMENT`) to adjust tracking offsets, decrementing the offset counter so portal contents bypass standard container index assumptions. Once element trees and insertions complete, accumulated lifecycle callbacks are executed. Layout effects (`useLayoutEffect`) and insertion effects (`useInsertionEffect`) fire synchronously, while standard effects (`useEffect`) are scheduled asynchronously via `requestAnimationFrame`.

```typescript
    if (child.tag === HONO_PORTAL_ELEMENT) {
      offset--
    } else if (isNew) {
      if (!el.parentNode) {
        container.appendChild(el)
      }
    } else if (childNodes[offset] !== el && childNodes[offset - 1] !== el) {
      if (childNodes[offset + 1] === el) {
        container.appendChild(childNodes[offset])
      } else {
        container.insertBefore(el, insertBeforeNode || childNodes[offset] || null)
      }
    }
```
Sources: [src/jsx/dom/render.ts:444-457](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L444-L457)

## Core State and Lifecycle Hooks

### Stash Indices and State Hooks

Client hook execution relies on internal stash arrays mapped to each component node via the `DOM_STASH` symbol constant. Within `src/jsx/hooks/index.ts`, stashes are categorized using specific numeric indices to segregate different hook types.

| Stash Index Constant | Numerical Value | Target Hook Type |
| :--- | :--- | :--- |
| `STASH_SATE` | `0` | State hooks (`useState`) |
| `STASH_EFFECT` | `1` | Effect hooks (`useEffect`, `useLayoutEffect`, `useInsertionEffect`) |
| `STASH_CALLBACK` | `2` | Callback hooks (`useCallback`) |
| `STASH_MEMO` | `3` | Memoization hooks (`useMemo`) |
| `STASH_REF` | `4` | Reference hooks (`useRef`) |

Sources: [src/jsx/constants.ts:3](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/constants.ts#L3), [src/jsx/hooks/index.ts:8-12](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L8-L12)

The `useState` implementation resolves its initial state by evaluating initializer functions if provided, checking whether `buildDataStack` contains an active node object context. If no active build data exists, it returns the initial state paired with a no-op updater. Otherwise, it retrieves or initializes the state array inside the node stash, increments the hook index counter, and stores a state-updater tuple.

```typescript
export const useState: UseStateType = <T>(
  initialState?: T | (() => T)
): [T, UpdateStateFunction<T>] => {
  const resolveInitialState = () =>
    typeof initialState === 'function' ? (initialState as () => T)() : (initialState as T)

  const buildData = buildDataStack.at(-1) as [unknown, NodeObject]
  if (!buildData) {
    return [resolveInitialState(), () => {}]
  }
  const [, node] = buildData

  const stateArray = (node[DOM_STASH][1][STASH_SATE] ||= [])
  const hookIndex = node[DOM_STASH][0]++

  return (stateArray[hookIndex] ||= [
    resolveInitialState(),
    (newState: T | ((currentState: T) => T)) => {
      const localUpdateHook = updateHook
      const stateData = stateArray[hookIndex]
      if (typeof newState === 'function') {
        newState = (newState as (currentState: T) => T)(stateData[0])
      }

```
Sources: [src/jsx/hooks/index.ts:182-205](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L182-L205)

### Dependency Resolution and Effect Scheduling

Dependency arrays are validated via `isDepsChanged`, which evaluates whether previous dependencies are undefined, length mismatches occur, or any individual dependency reference fails strict equality checks (`dep !== prevDeps[i]`).

```typescript
const isDepsChanged = (
  prevDeps: readonly unknown[] | undefined,
  deps: readonly unknown[] | undefined
): boolean =>
  !prevDeps ||
  !deps ||
  prevDeps.length !== deps.length ||
  deps.some((dep, i) => dep !== prevDeps[i])

```
Sources: [src/jsx/hooks/index.ts:27-35](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L27-L35)

Effect metadata is encapsulated within `EffectData` tuples, holding dependency arrays alongside callbacks for layout effects, cleanups, standard effects, and insertion effects.

```typescript
export type EffectData = [
  readonly unknown[] | undefined, // deps
  (() => void | (() => void)) | undefined, // layout effect
  (() => void) | undefined, // cleanup
  (() => void) | undefined, // effect
  (() => void) | undefined, // insertion effect
]
```
Sources: [src/jsx/hooks/index.ts:14-20](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L14-L20)

> [!NOTE]
> When `applyNodeObject` flushes accumulated callbacks, insertion effects and layout effects execute synchronously before paint, while standard `useEffect` callbacks are deferred using `requestAnimationFrame`.

Sources: [src/jsx/dom/render.ts:462-480](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts#L462-L480)

## Concurrent and Store Hooks

### Transitions and View Transitions

Advanced concurrency in Hono's [JSX Runtime](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/jsx-and-ui/jsx-runtime) coordinates asynchronous renders via `startTransition`, `useTransition`, and view transitions. The `runCallback` helper wraps asynchronous execution by pushing a `[type, promise]` tuple onto the module-level `pendingStack`. Specifically, `startTransition` invokes `runCallback` with pending type `1`, whereas `startTransitionHook` registers pending type `2`.

```typescript
// PendingType is defined in "../dom/render", 3 is used for useDeferredValue
const pendingStack: [PendingType | 3, Promise<void>][] = []
const runCallback = (type: PendingType, callback: Function): void => {
  let resolve: (() => void) | undefined
  const promise = new Promise<void>((r) => (resolve = r))
  pendingStack.push([type, promise])
  try {
    const res = callback()
    if (res instanceof Promise) {
      res.then(resolve, resolve)
    } else {
      resolve!()
    }
  } finally {
    pendingStack.pop()
  }
}

export const startTransition = (callback: () => void): void => {
  runCallback(1, callback)
}
const startTransitionHook = (callback: () => void | Promise<void>): void => {
  runCallback(2, callback)
}
```
Sources: [src/jsx/hooks/index.ts:101-124](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L101-L124)

The `useTransition` hook reads the current active build stack frame to determine [execution context](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/core-engine/execution-context). It maintains local error and boolean state variables via `useState`. When a state update occurs within a transition callback, `startTransitionLocalHook` triggers an internal boolean toggle and catches any asynchronous errors to propagate them into the component's error boundary stack.

```typescript
export const useTransition = (): [boolean, (callback: () => void | Promise<void>) => void] => {
  const buildData = buildDataStack.at(-1) as [Context, NodeObject]
  if (!buildData) {
    return [false, () => {}]
  }

  const [error, setError] = useState<[Error]>()
  const [state, updateState] = useState<boolean>()
  if (error) {
    throw error[0]
  }
  const startTransitionLocalHook = useCallback<typeof startTransitionHook>(
    (callback) => {
      startTransitionHook(() => {
        updateState((state) => !state)
        let res = callback()
        if (res instanceof Promise) {
          res = res.catch((e) => {
            setError([e])
          })
        }
        return res
      })
    },
    [state]
  )

  const [context] = buildData
  return [context[0] === 2, startTransitionLocalHook]
}
```
Sources: [src/jsx/hooks/index.ts:126-155](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L126-L155)

View transitions leverage browser-native `document.startViewTransition` APIs where supported, falling back to immediate synchronous execution and resolved promise stubs if the API is absent.

```typescript
const documentStartViewTransition: (cb: () => void) => { finished: Promise<void> } = (cb) => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  if ((document as any)?.startViewTransition) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (document as any).startViewTransition(cb)
  } else {
    cb()
    return { finished: Promise.resolve() }
  }
}
```
Sources: [src/jsx/hooks/index.ts:43-52](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L43-L52)

The `viewTransitionHook` coordinates two-phase updates, checking node version stamps (`node.vC`) before and after DOM commitment to safely execute transition callbacks while mutating `viewTransitionState`.

Sources: [src/jsx/hooks/index.ts:55-77](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L55-L77)

> [!NOTE]
> The `useViewTransition` hook returns a boolean indicating transition activity along with the `startViewTransition` dispatcher, returning `[false, () => {}]` safely if called outside an active render tree build.

Sources: [src/jsx/hooks/index.ts:89-99](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L89-L99)

### Deferred Values

The `useDeferredValue` hook delays value updates during concurrent rendering passes. It accepts a target value and optional initial state, maintaining a tuple of `[cachedValue, currentValue]` in internal state. If the incoming value matches the cached secondary value via `Object.is`, it returns immediately. Otherwise, it pushes pending type `3` onto `pendingStack`, assigns a temporary `updateHook` to update the deferred reference once children finish rendering, and updates state.

```typescript
type UseDeferredValue = <T>(value: T, initialValue?: T) => T
export const useDeferredValue: UseDeferredValue = <T>(value: T, ...rest: [T | undefined]): T => {
  const [values, setValues] = useState<[T, T]>(
    (rest.length ? [rest[0], rest[0]] : [value, value]) as [T, T]
  )
  if (Object.is(values[1], value)) {
    return values[1]
  }

  pendingStack.push([3, Promise.resolve()])
  updateHook = async (context: Context, _, cb: (context: Context) => void) => {
    cb(context)
    values[0] = value
  }
  setValues([values[0], value])
  updateHook = undefined
  pendingStack.pop()

  return values[0]
}
```
Sources: [src/jsx/hooks/index.ts:157-176](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts#L157-L176)

## Client Context and Error Handling

### Scoped Context Propagation

Context propagation in Hono's DOM JSX implementation relies on factory-generated provider functions created via `createContextProviderFunction`. When a context provider renders, it pushes its current `value` onto the module-level `values` array and appends an internal marker node equipped with `DOM_INTERNAL_TAG` to execute `values.push(value)`. 

```typescript
export const createContextProviderFunction =
  <T>(values: T[]): Function =>
  ({ value, children }: { value: T; children: Child[] }) => {
    if (!children) {
      return undefined
    }

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const props: { children: any } = {
      children: [
        {
          tag: setInternalTagFlag(() => {
            values.push(value)
          }),
          props: {},
        },
      ],
    }
```
Sources: [src/jsx/dom/context.ts:7-24](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/context.ts#L7-L24)

After processing the children array, the provider appends a closing internal node that pops the value off the stack (`values.pop()`). Furthermore, the returned fragment object attaches a `DOM_ERROR_HANDLER` symbol property to intercept failures, ensuring that if rendering throws an exception within the provider subtree, the context stack value is correctly popped before the error bubbles upward.

```typescript
  props.children.push({
    tag: setInternalTagFlag(() => {
      values.pop()
    }),
    props: {},
  })
  const res = { tag: '', props, type: '' }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => {
    values.pop()
    throw err
  }
  return res
}
```
Sources: [src/jsx/dom/context.ts:30-43](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/context.ts#L30-L43)

The base `createContext` function initializes this values array with the provided `defaultValue`, wraps it in `createContextProviderFunction`, appends the resulting context object to the global `globalContexts` registry, and exposes `.Provider` pointing directly to the provider function.

```typescript
export const createContext = <T>(defaultValue: T): Context<T> => {
  const values = [defaultValue]
  const context: Context<T> = createContextProviderFunction(values) as Context<T>
  context.values = values
  context.Provider = context
  globalContexts.push(context as Context<unknown>)
  return context
}
```
Sources: [src/jsx/dom/context.ts:45-52](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/context.ts#L45-L52)

> [!NOTE]
> The `DOM_INTERNAL_TAG` flag is set via `setInternalTagFlag` to mark push and pop operations, distinguishing context boundary nodes from regular intrinsic elements during tree construction.

Sources: [src/jsx/dom/context.ts:18-31](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/context.ts#L18-L31), [src/jsx/dom/utils.ts:3-6](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/utils.ts#L3-L6)

### Error Boundary and Suspense Coordination

Component error management and asynchronous chunk loading are coordinated by specialized wrapper components that attach error interception hooks through the `DOM_ERROR_HANDLER` symbol. The `ErrorBoundary` component wraps its children in a fragment and assigns an error handler that checks whether the caught error is a `Promise`. If it represents a promise, it rethrows it to propagate asynchronous suspension; otherwise, it invokes any custom `onError` callback and renders either the `fallbackRender` result or the static `fallback` node.

```typescript
export const ErrorBoundary: FC<
  PropsWithChildren<{
    fallback?: Child
    fallbackRender?: FallbackRender
    onError?: ErrorHandler
  }>
> = (({ children, fallback, fallbackRender, onError }: any) => {
  const res = Fragment({ children })
  ;(res as any)[DOM_ERROR_HANDLER] = (err: any) => {
    if (err instanceof Promise) {
      throw err
    }
    onError?.(err)
    return fallbackRender?.(err) || fallback
  }
  return res
}) as any
```
Sources: [src/jsx/dom/components.ts:7-23](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L7-L23)

Conversely, the `Suspense` component expects thrown values to be promises. Its `DOM_ERROR_HANDLER` implementation validates that the caught error is an instance of `Promise`; if a non-promise error is thrown, it bypasses suspension and rethrows the error. When a valid promise is caught, it attaches a `.finally(retry)` listener to trigger a re-render attempt once the asynchronous resource resolves, returning the fallback element immediately.

```typescript
export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = (({
  children,
  fallback,
}: any) => {
  const res = Fragment({ children })
  ;(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => {
    if (!(err instanceof Promise)) {
      throw err
    }
    err.finally(retry)
    return fallback
  }
  return res
}) as any
```
Sources: [src/jsx/dom/components.ts:25-38](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L25-L38)

| Component Symbol | Target Property | Error Type Handling | Fallback Resolution |
| :--- | :--- | :--- | :--- |
| `ErrorBoundary` | `DOM_ERROR_HANDLER` | Rethrows if `err instanceof Promise`; invokes `onError(err)` for synchronous errors. | Returns `fallbackRender?.(err)` or static `fallback`. |
| `Suspense` | `DOM_ERROR_HANDLER` | Throws if `!(err instanceof Promise)`; chains `err.finally(retry)`. | Returns static `fallback`. |

Sources: [src/jsx/dom/components.ts:7-38](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L7-L38), [src/jsx/constants.ts:2](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/constants.ts#L2)

## DOM Specific Intrinsic Elements

### Document Metadata Hoisting

Hono provides client-side functional components for document metadata tags—specifically `title`, `script`, `style`, `link`, and `meta`—that intercept standard rendering to manage head elements dynamically. The internal helper `documentMetadataTag` queries `document.head` to locate existing tags and checks for deduplication keys defined in `deDupeKeyMap`.

```typescript
const documentMetadataTag = (
  tag: string,
  props: Props,
  preserveNodeType: PreserveNodeType | undefined,
  supportSort: boolean,
  supportBlocking: boolean
) => {
  if (props?.itemProp) {
    return {
      tag,
      props,
      type: tag,
      ref: props.ref,
    }
  }

  const head = document.head

  let { onLoad, onError, precedence, blocking, ...restProps } = props
  let element: HTMLElement | null = null
  let created = false

  const deDupeKeys = deDupeKeyMap[tag]
  const deDupeByKey = shouldDeDupeByKey(tag, supportSort)
```
Sources: [src/jsx/dom/intrinsic-element/components.ts:58-81](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/intrinsic-element/components.ts#L58-L81)

When deduplication matches an existing element, the component updates or reuses the node instead of appending a duplicate. For stylesheets and scripts, options like `precedence` and `blocking='render'` can coordinate asset insertion order and suspend rendering via `use(promise)` until stylesheets or blocking resources load.

```typescript
if (supportBlocking && blocking === 'render') {
  const key = deDupeKeyMap[tag][0]
  if (key && props[key]) {
    const value = props[key]
    const promise = (blockingPromiseMap[value] ||= new Promise<Event>((resolve, reject) => {
      insert(element as HTMLElement)
      element!.addEventListener('load', resolve)
      element!.addEventListener('error', reject)
    }))
    use(promise)
  }
}
```
Sources: [src/jsx/dom/intrinsic-element/components.ts:223-234](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/intrinsic-element/components.ts#L223-L234)

| Intrinsic Component | Deduplication Strategy | Preserve Node Type (`preserveNodeType`) | Blocking Support |
| :--- | :--- | :--- | :--- |
| `title` | Checked via `documentMetadataTag` (non-SVG) | `undefined` | No |
| `script` | Checked if `src` and `async` are present | `1` (preserve self) | Yes |
| `style` | Checked if `href` and `precedence` are present | `2` (preserve self and children) | Yes |
| `link` | Checked if stylesheet without disable/precedence omissions | `1` (preserve self) | Yes |
| `meta` | Checked via `documentMetadataTag` | `undefined` | No |

Sources: [src/jsx/dom/intrinsic-element/components.ts:257-316](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/intrinsic-element/components.ts#L257-L316)

> [!NOTE]
> If a metadata component specifies an `itemProp` attribute, it bypasses head-hoisting and deduplication entirely, returning a standard intrinsic element node.

Sources: [src/jsx/dom/intrinsic-element/components.ts:65-72](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/intrinsic-element/components.ts#L65-L72)

### Form Actions and Dedicated Hooks

Client DOM hooks provide state management primitives tailored for forms and asynchronous action handlers. The `FormContext` object maintains a `FormStatus` state containing a `pending` boolean flag, submitted `data`, HTTP `method` (`get` or `post`), and the `action` handler.

```typescript
type FormStatus =
  | {
      pending: false
      data: null
      method: null
      action: null
    }
  | {
      pending: true
      data: FormData
      method: 'get' | 'post'
      action: string | ((formData: FormData) => void | Promise<void>)
    }
export const FormContext: Context<FormStatus> = createContext<FormStatus>({
  pending: false,
  data: null,
  method: null,
  action: null,
})
```
Sources: [src/jsx/dom/hooks/index.ts:11-29](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/hooks/index.ts#L11-L29)

Active actions are tracked in a module-level `actions` set via `registerAction`, ensuring pending promises can be awaited to coordinate optimistic updates. The `useFormStatus` hook consumes `FormContext`, while `useOptimistic` uses the active actions list to automatically revert optimistic states once all pending promises settle.

```typescript
const actions: Set<Promise<unknown>> = new Set()
export const registerAction = (action: Promise<unknown>) => {
  actions.add(action)
  action.finally(() => actions.delete(action))
}

/**
 * This hook returns the current form status
 * @returns FormStatus
 */
export const useFormStatus = (): FormStatus => {
  return useContext(FormContext)
}

/**
 * This hook returns the current state and a function to update the state optimistically
 * The current state is updated optimistically and then reverted to the original state when all actions are resolved
 * @param state
 * @param updateState
 * @returns [T, (action: N) => void]
 */
export const useOptimistic = <T, N>(
  state: T,
  updateState: (currentState: T, action: N) => T
): [T, (action: N) => void] => {
  const [optimisticState, setOptimisticState] = useState(state)
  if (actions.size > 0) {
    Promise.all(actions).finally(() => {
      setOptimisticState(state)
    })
  } else {
    setOptimisticState(state)
  }
```
Sources: [src/jsx/dom/hooks/index.ts:31-63](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/hooks/index.ts#L31-L63)

> [!WARNING]
> The `useActionState` hook assigns the `PERMALINK` symbol property onto the returned action wrapper function to track permalink strings across form submissions.

Sources: [src/jsx/dom/hooks/index.ts:79-90](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/hooks/index.ts#L79-L90)

## Related

- [JSX Runtime](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/jsx-and-ui/jsx-runtime)
- [CSS Styling](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/jsx-and-ui/css-styling)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/llms.txt) for all pages in this wiki.
