Skip to main content
doc0blade47/honodocumentation home
Sign in

JSX Runtime

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Hono's JSX runtime provides both server-side rendering and client-side DOM execution models JSX Components and Context, Client DOM and Hooks. It processes functional components and intrinsic tags through unified entry points src/jsx/jsx-runtime.ts:6-8, src/jsx/jsx-dev-runtime.ts:12-25, validates and escapes attributes src/jsx/jsx-runtime.ts:16-49, and serializes hierarchical nodes into string buffers or browser DOM trees src/jsx/base.ts:249-302, src/jsx/dom/render.ts:41-80.

Sources: src/jsx/jsx-runtime.ts:6-8, src/jsx/jsx-dev-runtime.ts:12-25, src/jsx/base.ts:249-302, src/jsx/dom/render.ts:41-80, src/jsx/jsx-runtime.ts:16-49

JSX Runtime Entry Points

Automatic and Classic Runtimes

Hono supports both automatic (react-jsx) and development JSX transforms through dedicated module entry points runtime-tests/deno-jsx/deno.react-jsx.json:3-4, src/jsx/jsx-runtime.ts:6-8. The primary production runtime module hono/jsx/jsx-runtime exports jsx, jsxs, and Fragment directly from jsx-dev-runtime, alongside jsxTemplate (aliased from html) and utility helpers such as jsxAttr and jsxEscape [[src/jsx/jsx-runtime.ts:6-14], [src/jsx/jsx-runtime.ts:51]].

export { jsxDEV as jsx, Fragment } from './jsx-dev-runtime'
export { jsxDEV as jsxs } from './jsx-dev-runtime'
export type { JSX } from './jsx-dev-runtime'
import { html, raw } from '../helper/html'

Sources: src/jsx/jsx-runtime.ts:6-9

The development runtime function jsxDEV accepts a tag, a props dictionary, and an optional key, normalizing single child properties and arrays into structured node hierarchies via jsxFn src/jsx/jsx-dev-runtime.ts:12-25.

export function jsxDEV(
  tag: string | Function,
  props: Record<string, unknown>,
  key?: string
): JSXNode {
  let node: JSXNode
  if (!props || !('children' in props)) {
    node = jsxFn(tag, props, [])
  } else {
    const children = props.children as string | HtmlEscapedString
    node = Array.isArray(children) ? jsxFn(tag, props, children) : jsxFn(tag, props, [children])
  }
  node.key = key
  return node
}

Sources: src/jsx/jsx-dev-runtime.ts:12-25

Note

When configured with Deno and TypeScript compiler options like "jsx": "react-jsx" and "jsxImportSource": "hono/jsx", compiler targets resolve module specifiers against hono/jsx/jsx-runtime [[runtime-tests/deno-jsx/deno.react-jsx.json:3-4], [runtime-tests/deno-jsx/deno.react-jsx.json:10]].

Sources: runtime-tests/deno-jsx/deno.react-jsx.json:3-4, runtime-tests/deno-jsx/deno.react-jsx.json:10

Attribute Processing and Exports

The jsxAttr function evaluates attribute keys and dynamic values, checking attribute name validity and formatting styles, numbers, promises, or raw escaped strings into valid markup attributes src/jsx/jsx-runtime.ts:16-49.

Parameter / ExportType / BehaviorSource Reference
jsx / jsxsAliased to jsxDEV for automatic JSX compilationsrc/jsx/jsx-runtime.ts:6-7
FragmentComponent grouping container exported from dev runtimesrc/jsx/jsx-runtime.ts:6
jsxTemplateAliased to html helper for template literalssrc/jsx/jsx-runtime.ts:14
jsxAttrFunction validating attribute keys and formatting valuessrc/jsx/jsx-runtime.ts:16-49
jsxEscapeIdentity helper function returning raw string valuessrc/jsx/jsx-runtime.ts:51

Sources: src/jsx/jsx-runtime.ts:6-14, src/jsx/jsx-runtime.ts:16-49, src/jsx/jsx-runtime.ts:51

JSX Node Hierarchy

Node Factories and Creation

The jsx factory function accepts a tag, a props dictionary, and variadic children, handling prop initialization and key extraction before delegating to jsxFn src/jsx/base.ts:305-317. The jsxFn function evaluates the tag type to instantiate the appropriate hierarchy node src/jsx/base.ts:320-352.

export const jsx = (tag: string | Function, props: Props | null, ...children: Child[]): JSXNode => {
  props ??= {}
  if (children.length) {
    props.children = children.length === 1 ? children[0] : children
  }
 
  const key = props.key
  delete props['key']
 
  const node = jsxFn(tag, props, children)
  node.key = key
  return node
}

Sources: src/jsx/base.ts:305-317

When tag is a function, jsxFn returns a JSXFunctionNode src/jsx/base.ts:330-331. Intrinsic element tags from lookup dictionaries instantiate JSXFunctionNode mapped to their intrinsic renderer src/jsx/base.ts:332-337. Namespace-sensitive tags such as svg and head wrap children in a namespace context component node inside a base JSXNode src/jsx/base.ts:338-349, while remaining tags construct a standard JSXNode src/jsx/base.ts:350-352.

Note

On the initial call to jsxFn, domRenderers bindings are assigned to intrinsic element tags using the DOM_RENDERER symbol property, initializing the client DOM rendering pipeline src/jsx/base.ts:319-328.

Sources: src/jsx/base.ts:319-328

Node Types and Specialization

Specialized node classes extend the hierarchy to handle functional evaluation and document fragments src/jsx/base.ts:255-303. The JSXFunctionNode executes functional components by invoking the component function with its properties and children, processing return values like promises, nested nodes, or escaped strings src/jsx/base.ts:255-296. The JSXFragmentNode implements container nodes that serialize their children directly to buffers without rendering surrounding tags src/jsx/base.ts:299-303.

class JSXFunctionNode extends JSXNode {
  override toStringToBuffer(buffer: StringBufferWithCallbacks): void {
    const { children } = this
 
    const props = { ...this.props }
    if (children.length) {
      props.children = children.length === 1 ? children[0] : children
    }
 
    const res = (this.tag as Function).call(null, props)
    // ...
  }
}

Sources: src/jsx/base.ts:255-265

React-compatible element definitions and validation helpers support element checking and tree cloning [[src/jsx/types.ts:19-23], [src/jsx/base.ts:422-443]]. The isValidElement function validates unknown inputs src/jsx/base.ts:422-424, and cloneElement merges new properties and children into existing element nodes src/jsx/base.ts:426-443. Memoized functional components (memo) wrap components with shallowEqual checks to skip redundant computations src/jsx/base.ts:354-405.

Utility / ClassTarget Structure / PurposeSource Reference
JSXNodeBase structure for intrinsic and generic element nodessrc/jsx/base.ts:338-352
JSXFunctionNodeExecutes functional components and intrinsic tag rendererssrc/jsx/base.ts:255-296
JSXFragmentNodeRenders fragment children without enclosing HTML tagssrc/jsx/base.ts:299-303
isValidElementType guard checking for tag and props propertiessrc/jsx/base.ts:422-424
cloneElementClones elements with merged property overridessrc/jsx/base.ts:426-443
memoWraps functional components with memoization and DOM_MEMOsrc/jsx/base.ts:384-405

Sources: src/jsx/base.ts:255-303, src/jsx/base.ts:354-443, src/jsx/types.ts:19-23

Attribute Normalization and Escaping

Attribute Validation and Processing

The runtime formats and serializes element attributes using the jsxAttr utility src/jsx/jsx-runtime.ts:16-49. Before processing any value, jsxAttr validates the attribute name with isValidAttributeName; if validation fails, it returns an empty raw string src/jsx/jsx-runtime.ts:20-22. When the attribute name is valid, it initializes a string buffer formatted as key=" src/jsx/jsx-runtime.ts:23.

export const jsxAttr = (
  key: string,
  v: string | Promise<string> | Record<string, string | number | null | undefined | boolean>
): HtmlEscapedString | Promise<HtmlEscapedString> => {
  if (!isValidAttributeName(key)) {
    return raw('')
  }
  const buffer: StringBuffer = [`${key}="`] as StringBuffer
  // ...

Sources: src/jsx/jsx-runtime.ts:16-23

Style Stringification and Value Types

Attribute values are handled differently based on their runtime types src/jsx/jsx-runtime.ts:24-46. If the attribute key is style and the value is an object, styleObjectForEach iterates over the style properties, skipping null or undefined entries, separating properties with semicolons, and escaping the resulting string into the buffer src/jsx/jsx-runtime.ts:24-33.

if (key === 'style' && typeof v === 'object') {
  // object to style strings
  let styleStr = ''
  styleObjectForEach(v as Record<string, string | number>, (property, value) => {
    if (value != null) {
      styleStr += `${styleStr ? ';' : ''}${property}:${value}`
    }
  })
  escapeToBuffer(styleStr, buffer)
  buffer[0] += '"'

Sources: src/jsx/jsx-runtime.ts:24-33

Note

Null and undefined attribute values are suppressed entirely, returning an empty raw string instead of rendering empty attributes src/jsx/jsx-runtime.ts:37-38.

String values undergo HTML escaping via escapeToBuffer before closing quotation marks are appended src/jsx/jsx-runtime.ts:34-36. Numbers and already-escaped HTML strings append directly without re-escaping src/jsx/jsx-runtime.ts:39-40. Promise instances unshift the promise into the buffer for asynchronous resolution src/jsx/jsx-runtime.ts:41-42, while other types are converted to strings and escaped src/jsx/jsx-runtime.ts:43-45.

Value TypeProcessing BehaviorSource Reference
Invalid NameReturns raw('') immediatelysrc/jsx/jsx-runtime.ts:20-22
Style ObjectIterates properties, formats property:value; pairs, escapes buffersrc/jsx/jsx-runtime.ts:24-33
stringEscapes string content via escapeToBuffersrc/jsx/jsx-runtime.ts:34-36
null / undefinedSuppresses attribute, returns raw('')src/jsx/jsx-runtime.ts:37-38
number / isEscapedAppends directly to buffer without escapingsrc/jsx/jsx-runtime.ts:39-40
PromiseUnshifts promise into buffer for async evaluationsrc/jsx/jsx-runtime.ts:41-42

Sources: src/jsx/jsx-runtime.ts:20-46

Buffer Stringification Pipeline

Node Serialization and Promise Interception

Tree serialization converts hierarchical node structures into contiguous string chunks using a mutable StringBufferWithCallbacks buffer. The process evaluates functional components through JSXFunctionNode.toStringToBuffer, invoking component functions with props and child elements src/jsx/base.ts:255-264. When a component return value is a Promise, the pipeline intercepts it to support asynchronous rendering src/jsx/base.ts:269. If global contexts are active, the implementation captures current context bindings via globalContexts.map to preserve them across asynchronous yields src/jsx/base.ts:273-274. It then unshifts the promise into the buffer alongside an asynchronous callback that attaches captured local contexts to resolved node instances src/jsx/base.ts:275-283.

} else if (res instanceof Promise) {
  if (globalContexts.length === 0) {
    buffer.unshift('', res)
  } else {
    // save current contexts for resuming
    const currentContexts: LocalContexts = globalContexts.map((c) => [c, c.values.at(-1)])
    buffer.unshift(
      '',
      res.then((childRes) => {
        if (childRes instanceof JSXNode) {
          childRes.localContexts = currentContexts
        }
        return childRes
      })
    )
  }

Sources: src/jsx/base.ts:269-284

Warning

Async components executing without active global contexts omit the context-capturing map step, directly unshifting the promise to minimize overhead src/jsx/base.ts:270-272.

Primitive Handling and Fragment Flattening

Non-promise component outputs undergo strict type checks before buffer insertion src/jsx/base.ts:266-295. Booleans, null, and undefined values yield immediate termination without appending anything to the buffer src/jsx/base.ts:266-268. Fragment nodes delegate traversal directly to childrenToStringToBuffer src/jsx/base.ts:299-302, flattening child collections without introducing wrapper elements.

export class JSXFragmentNode extends JSXNode {
  override toStringToBuffer(buffer: StringBufferWithCallbacks): void {
    childrenToStringToBuffer(this.children, buffer)
  }
}

Sources: src/jsx/base.ts:299-303]

Numbers and pre-escaped objects (isEscaped) append directly to the current buffer chunk, and any associated rendering callbacks are pushed onto the buffer's callback array src/jsx/base.ts:287-292. All other non-escaped primitives pass through escapeToBuffer to guarantee safety src/jsx/base.ts:294.

Return TypeSerialization ActionSource Reference
boolean / null / undefinedDiscards output and returns immediatelysrc/jsx/base.ts:266-268
PromiseUnshifts promise and context capture onto the buffersrc/jsx/base.ts:269-284
JSXNodeRecursively invokes toStringToBuffer on the nodesrc/jsx/base.ts:285-286
number / isEscapedAppends directly and preserves evaluation callbackssrc/jsx/base.ts:287-292
Other TypesEncodes value via escapeToBuffersrc/jsx/base.ts:294

Sources: src/jsx/base.ts:266-296, src/jsx/base.ts:299-303

Child Collection Traversal

Array Normalization and Children Object Utilities

The toArray utility normalizes child node inputs into an array representation, wrapping non-array elements in a single-element array while passing arrays through unchanged src/jsx/children.ts:3-4. The Children namespace aggregates utility methods designed for functional inspection and transformation of child node trees src/jsx/children.ts:5-20.

export const toArray = (children: Child): Child[] =>
  Array.isArray(children) ? children : [children]
export const Children = {
  map: (children: Child[], fn: (child: Child, index: number) => Child): Child[] =>
    toArray(children).map(fn),
  forEach: (children: Child[], fn: (child: Child, index: number) => void): void => {
    toArray(children).forEach(fn)
  },
  count: (children: Child[]): number => toArray(children).length,
  only: (_children: Child[]): Child => {
    const children = toArray(_children)
    if (children.length !== 1) {
      throw new Error('Children.only() expects only one child')
    }
    return children[0]
  },
  toArray,
}

Sources: src/jsx/children.ts:3-20

MethodParametersReturn TypeDescriptionSources Reference
toArraychildren: ChildChild[]Normalizes single child or array into an arraysrc/jsx/children.ts:3-4
Children.mapchildren: Child[], fn: (child: Child, index: number) => ChildChild[]Maps over normalized child collectionsrc/jsx/children.ts:6-7
Children.forEachchildren: Child[], fn: (child: Child, index: number) => voidvoidIterates through normalized child collectionsrc/jsx/children.ts:8-10
Children.countchildren: Child[]numberReturns the total count of normalized childrensrc/jsx/children.ts:11
Children.only_children: Child[]ChildAsserts exactly one child exists and returns itsrc/jsx/children.ts:12-18
Children.toArraychildren: ChildChild[]Alias to module-level toArraysrc/jsx/children.ts:19

Sources: src/jsx/children.ts:3-20

Warning

Children.only evaluates normalized length and throws an explicit Error with message 'Children.only() expects only one child' if the count is anything other than 1 src/jsx/children.ts:12-17.

Sources: src/jsx/children.ts:12-18

Client JSX Runtime Adapters

DOM-Targeted JSX Factories and Server Emulation

The hono/jsx/dom runtime supplies adapters tailored for interactive client-side rendering and compatibility APIs mimicking react-dom/server [[src/jsx/dom/jsx-dev-runtime.ts:3-4], [src/jsx/dom/server.ts:3-4]]. The jsxDEV function constructs virtual DOM nodes while checking tag lookups against intrinsic element components src/jsx/dom/jsx-dev-runtime.ts:10-13. If a string tag matches an entry in intrinsic-element/components, the factory substitutes it before returning the descriptor object containing tag, type, props, key, and ref src/jsx/dom/jsx-dev-runtime.ts:11-20. The Fragment factory constructs an empty tag node utilizing jsxDEV with an empty string src/jsx/dom/jsx-dev-runtime.ts:23. The runtime exports jsx, jsxs, and Fragment through standard and dev entry modules src/jsx/dom/jsx-runtime.ts:6-8.

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-23

Server-side rendering adapters in hono/jsx/dom/server provide renderToString and renderToReadableStream functions src/jsx/dom/server.ts:65. The renderToString implementation evaluates element stringification, logging a warning if options are provided, and throws an error if an asynchronous component is encountered src/jsx/dom/server.ts:21-30. The renderToReadableStream utility passes non-object elements through string conversion and delegates streaming output to renderToReadableStreamHono src/jsx/dom/server.ts:50-63.

const renderToString = (element: Child, options: RenderToStringOptions = {}): string => {
  if (Object.keys(options).length > 0) {
    console.warn('options are not supported yet')
  }
  const res = element?.toString() ?? ''
  if (typeof res !== 'string') {
    throw new Error('Async component is not supported in renderToString')
  }
  return res
}

Sources: src/jsx/dom/server.ts:21-30

Warning

renderToString throws an explicit Error with the message 'Async component is not supported in renderToString' if the evaluated result is not a primitive string src/jsx/dom/server.ts:26-28.

Sources: src/jsx/dom/server.ts:26-28]

DOM Error Boundaries, Suspense, and Context

Specialized components for the DOM runtime handle asynchronous operations, boundary error trapping, and state context propagation [[src/jsx/dom/components.ts:7-38], [src/jsx/dom/context.ts:7-51]]. The ErrorBoundary component attaches a DOM_ERROR_HANDLER property to fragment nodes that intercepts errors, invokes optional onError callbacks, and returns fallback UI via fallbackRender or fallback props src/jsx/dom/components.ts:7-23. The Suspense component attaches a handler that checks whether caught errors are instances of Promise; if not, they are rethrown, otherwise err.finally(retry) is scheduled and the fallback child is returned src/jsx/dom/components.ts:25-38.

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

Context providers use createContextProviderFunction to push and pop context values into an internal array during tree traversal src/jsx/dom/context.ts:7-43. When rendering, the provider injects internal tag nodes that mutate the shared values array before and after evaluating children, and registers an error handler to pop values if rendering throws src/jsx/dom/context.ts:17-42. The createContext function initializes the default value array, binds Provider, and registers the context inside globalContexts src/jsx/dom/context.ts:45-51.

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: {},
        },
      ],
    }
    if (Array.isArray(children)) {
      props.children.push(...children.flat())
    } else {
      props.children.push(children)
    }
    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:7-43

Was this page helpful?