> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/honojs/hono/llms.txt
> Use this file to discover all available pages before exploring further.

# Client-Side Rendering

> Build interactive web applications with Hono's client-side JSX rendering and React-compatible hooks

Hono's `hono/jsx/dom` module provides client-side rendering capabilities with a React-compatible API. It allows you to build interactive user interfaces that run in the browser.

## Setup

Configure your `tsconfig.json` for client-side JSX:

```json tsconfig.json theme={null}
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx/dom"
  }
}
```

## Basic Rendering

Use the `render()` function to mount components to the DOM:

```tsx theme={null}
import { render } from 'hono/jsx/dom'

const App = () => {
  return (
    <div>
      <h1>Hello from Hono JSX DOM!</h1>
      <p>This is rendered on the client side.</p>
    </div>
  )
}

// Render to the DOM
const root = document.getElementById('root')
if (root) {
  render(<App />, root)
}
```

Source: [src/jsx/dom/render.ts:763-766](https://github.com/honojs/hono/blob/main/src/jsx/dom/render.ts#L763-L766)

## Using createRoot (React 18+ API)

For better control over rendering, use the `createRoot` API:

```tsx theme={null}
import { createRoot } from 'hono/jsx/dom/client'

const App = () => {
  return (
    <div>
      <h1>My App</h1>
    </div>
  )
}

const root = createRoot(document.getElementById('root')!)
root.render(<App />)

// Later, you can update or unmount
// root.render(<UpdatedApp />)
// root.unmount()
```

Source: [src/jsx/dom/client.ts:23-66](https://github.com/honojs/hono/blob/main/src/jsx/dom/client.ts#L23-L66)

## Hooks

Hono JSX DOM supports all major React hooks:

### useState

Manage component state:

```tsx theme={null}
import { useState } from 'hono/jsx/dom'

const Counter = () => {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(count - 1)}>
        Decrement
      </button>
      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:182-243](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L182-L243)

### useEffect

Perform side effects:

```tsx theme={null}
import { useState, useEffect } from 'hono/jsx/dom'

const Timer = () => {
  const [seconds, setSeconds] = useState(0)
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)
    
    // Cleanup function
    return () => clearInterval(interval)
  }, []) // Empty deps array means run once on mount
  
  return <div>Elapsed: {seconds}s</div>
}
```

Source: [src/jsx/hooks/index.ts:288-289](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L288-L289)

### useRef

Access DOM elements directly:

```tsx theme={null}
import { useRef, useEffect } from 'hono/jsx/dom'

const FocusInput = () => {
  const inputRef = useRef<HTMLInputElement>(null)
  
  useEffect(() => {
    // Focus input on mount
    inputRef.current?.focus()
  }, [])
  
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Auto-focused" />
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:319-330](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L319-L330)

### useCallback

Memoize callback functions:

```tsx theme={null}
import { useState, useCallback } from 'hono/jsx/dom'

const TodoList = () => {
  const [todos, setTodos] = useState<string[]>([])
  const [input, setInput] = useState('')
  
  const addTodo = useCallback(() => {
    if (input.trim()) {
      setTodos([...todos, input])
      setInput('')
    }
  }, [input, todos])
  
  return (
    <div>
      <input 
        value={input} 
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo, i) => <li key={i}>{todo}</li>)}
      </ul>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:299-316](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L299-L316)

### useMemo

Memoize expensive computations:

```tsx theme={null}
import { useState, useMemo } from 'hono/jsx/dom'

const ExpensiveComponent = ({ items }: { items: number[] }) => {
  const [multiplier, setMultiplier] = useState(1)
  
  const sum = useMemo(() => {
    console.log('Computing sum...')
    return items.reduce((acc, item) => acc + item, 0) * multiplier
  }, [items, multiplier])
  
  return (
    <div>
      <p>Sum: {sum}</p>
      <button onClick={() => setMultiplier(m => m + 1)}>
        Multiply by {multiplier + 1}
      </button>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:348-363](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L348-L363)

### useReducer

Manage complex state logic:

```tsx theme={null}
import { useReducer } from 'hono/jsx/dom'

type State = { count: number }
type Action = { type: 'increment' | 'decrement' | 'reset' }

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    case 'reset':
      return { count: 0 }
    default:
      return state
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:245-258](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L245-L258)

### useContext

Access context values:

```tsx theme={null}
import { createContext, useContext, useState } from 'hono/jsx/dom'

type Theme = 'light' | 'dark'

const ThemeContext = createContext<Theme>('light')

const ThemeToggle = () => {
  const theme = useContext(ThemeContext)
  
  return (
    <button className={`btn-${theme}`}>
      Current theme: {theme}
    </button>
  )
}

const App = () => {
  const [theme, setTheme] = useState<Theme>('light')
  
  return (
    <ThemeContext.Provider value={theme}>
      <div>
        <ThemeToggle />
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          Toggle Theme
        </button>
      </div>
    </ThemeContext.Provider>
  )
}
```

### useLayoutEffect

Perform DOM measurements before paint:

```tsx theme={null}
import { useLayoutEffect, useRef, useState } from 'hono/jsx/dom'

const MeasuredDiv = () => {
  const ref = useRef<HTMLDivElement>(null)
  const [height, setHeight] = useState(0)
  
  useLayoutEffect(() => {
    if (ref.current) {
      setHeight(ref.current.offsetHeight)
    }
  }, [])
  
  return (
    <div>
      <div ref={ref}>Content to measure</div>
      <p>Height: {height}px</p>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:290-293](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L290-L293)

### useId

Generate unique IDs for accessibility:

```tsx theme={null}
import { useId } from 'hono/jsx/dom'

const FormField = ({ label }: { label: string }) => {
  const id = useId()
  
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:366](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L366)

## Event Handling

Handle user interactions with event handlers:

```tsx theme={null}
import { useState } from 'hono/jsx/dom'

const FormExample = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  })
  
  const handleSubmit = (e: Event) => {
    e.preventDefault()
    console.log('Form submitted:', formData)
  }
  
  const handleChange = (e: Event) => {
    const target = e.target as HTMLInputElement | HTMLTextAreaElement
    setFormData({
      ...formData,
      [target.name]: target.value
    })
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
    </form>
  )
}
```

Source: [src/jsx/dom/render.ts:115-133](https://github.com/honojs/hono/blob/main/src/jsx/dom/render.ts#L115-L133)

## Hydration

Hydrate server-rendered HTML:

```tsx theme={null}
import { hydrateRoot } from 'hono/jsx/dom/client'

const App = () => {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <h1>Server + Client Rendered</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

// Hydrate server-rendered content
const root = document.getElementById('root')!
hydrateRoot(root, <App />)
```

Source: [src/jsx/dom/client.ts:76-84](https://github.com/honojs/hono/blob/main/src/jsx/dom/client.ts#L76-L84)

<Note>
  In Hono's implementation, `hydrateRoot` is equivalent to `createRoot().render()`. The actual DOM is rendered from scratch.
</Note>

## Refs and DOM Access

### Callback Refs

```tsx theme={null}
const CallbackRefExample = () => {
  const setInputRef = (element: HTMLInputElement | null) => {
    if (element) {
      element.focus()
    }
  }
  
  return <input ref={setInputRef} type="text" />
}
```

### Ref Objects

```tsx theme={null}
import { createRef } from 'hono/jsx/dom'

const RefObjectExample = () => {
  const inputRef = createRef<HTMLInputElement>()
  
  const focusInput = () => {
    inputRef.current?.focus()
  }
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:371-373](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L371-L373)

### forwardRef

Forward refs to child components:

```tsx theme={null}
import { forwardRef } from 'hono/jsx/dom'
import type { RefObject } from 'hono/jsx/dom'

type InputProps = {
  placeholder?: string
}

const FancyInput = forwardRef<HTMLInputElement, InputProps>(
  ({ placeholder }, ref) => {
    return (
      <input
        ref={ref}
        type="text"
        placeholder={placeholder}
        className="fancy-input"
      />
    )
  }
)

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null)
  
  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Fancy input" />
      <button onClick={() => inputRef.current?.focus()}>
        Focus
      </button>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:375-382](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L375-L382)

## Suspense and Error Boundaries

### Suspense

Handle async operations:

```tsx theme={null}
import { Suspense } from 'hono/jsx/dom'

const AsyncComponent = async () => {
  const data = await fetchData()
  return <div>{data}</div>
}

const App = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AsyncComponent />
    </Suspense>
  )
}
```

### ErrorBoundary

Catch and handle errors:

```tsx theme={null}
import { ErrorBoundary } from 'hono/jsx/dom'

const ProblematicComponent = () => {
  throw new Error('Oops!')
}

const App = () => {
  return (
    <ErrorBoundary 
      fallback={<div>Something went wrong</div>}
      onError={(error) => console.error(error)}
    >
      <ProblematicComponent />
    </ErrorBoundary>
  )
}
```

## Transitions

### useTransition

Mark updates as transitions:

```tsx theme={null}
import { useState, useTransition } from 'hono/jsx/dom'

const SearchResults = ({ query }: { query: string }) => {
  const [isPending, startTransition] = useTransition()
  const [results, setResults] = useState<string[]>([])
  
  const handleSearch = (query: string) => {
    startTransition(() => {
      // This update is marked as non-urgent
      const newResults = performSearch(query)
      setResults(newResults)
    })
  }
  
  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <div>Searching...</div>}
      <ul>
        {results.map((result, i) => <li key={i}>{result}</li>)}
      </ul>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:126-155](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L126-L155)

### useDeferredValue

Defer rendering of non-critical updates:

```tsx theme={null}
import { useState, useDeferredValue } from 'hono/jsx/dom'

const SearchComponent = () => {
  const [input, setInput] = useState('')
  const deferredInput = useDeferredValue(input)
  
  return (
    <div>
      <input 
        value={input} 
        onChange={(e) => setInput(e.target.value)}
      />
      {/* This renders with the deferred value */}
      <ExpensiveList query={deferredInput} />
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:158-176](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L158-L176)

## Portals

Render children outside the parent hierarchy:

```tsx theme={null}
import { createPortal } from 'hono/jsx/dom'

const Modal = ({ children, isOpen }: { children: any; isOpen: boolean }) => {
  if (!isOpen) return null
  
  const modalRoot = document.getElementById('modal-root')!
  
  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    modalRoot
  )
}

const App = () => {
  const [isOpen, setIsOpen] = useState(false)
  
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      <Modal isOpen={isOpen}>
        <h2>Modal Title</h2>
        <p>Modal content</p>
        <button onClick={() => setIsOpen(false)}>Close</button>
      </Modal>
    </div>
  )
}
```

Source: [src/jsx/dom/render.ts:782-792](https://github.com/honojs/hono/blob/main/src/jsx/dom/render.ts#L782-L792)

## flushSync

Force synchronous updates:

```tsx theme={null}
import { flushSync } from 'hono/jsx/dom'

const Counter = () => {
  const [count, setCount] = useState(0)
  
  const incrementSync = () => {
    flushSync(() => {
      setCount(c => c + 1)
    })
    // DOM is updated synchronously at this point
    console.log('Count updated:', count + 1)
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementSync}>Increment Sync</button>
    </div>
  )
}
```

Source: [src/jsx/dom/render.ts:768-780](https://github.com/honojs/hono/blob/main/src/jsx/dom/render.ts#L768-L780)

## Advanced Hooks

### useSyncExternalStore

Subscribe to external stores:

```tsx theme={null}
import { useSyncExternalStore } from 'hono/jsx/dom'

const store = {
  listeners: new Set<() => void>(),
  value: 0,
  subscribe(listener: () => void) {
    this.listeners.add(listener)
    return () => this.listeners.delete(listener)
  },
  getSnapshot() {
    return this.value
  },
  increment() {
    this.value++
    this.listeners.forEach(l => l())
  }
}

const Counter = () => {
  const value = useSyncExternalStore(
    store.subscribe.bind(store),
    store.getSnapshot.bind(store)
  )
  
  return (
    <div>
      <p>Store value: {value}</p>
      <button onClick={() => store.increment()}>Increment</button>
    </div>
  )
}
```

Source: [src/jsx/hooks/index.ts:397-425](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L397-L425)

### use (React 19)

Unwrap promises in render:

```tsx theme={null}
import { use } from 'hono/jsx/dom'

const fetchUser = async (id: number) => {
  const res = await fetch(`/api/users/${id}`)
  return res.json()
}

const UserProfile = ({ userPromise }: { userPromise: Promise<User> }) => {
  const user = use(userPromise)
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

const App = () => {
  const userPromise = fetchUser(1)
  
  return (
    <Suspense fallback={<div>Loading user...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}
```

Source: [src/jsx/hooks/index.ts:332-346](https://github.com/honojs/hono/blob/main/src/jsx/hooks/index.ts#L332-L346)

## Full Example: Todo App

```tsx theme={null}
import { render } from 'hono/jsx/dom'
import { useState, useCallback } from 'hono/jsx/dom'

type Todo = {
  id: number
  text: string
  completed: boolean
}

const TodoApp = () => {
  const [todos, setTodos] = useState<Todo[]>([])
  const [input, setInput] = useState('')
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')
  
  const addTodo = useCallback(() => {
    if (input.trim()) {
      setTodos([
        ...todos,
        { id: Date.now(), text: input, completed: false }
      ])
      setInput('')
    }
  }, [input, todos])
  
  const toggleTodo = useCallback((id: number) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }, [todos])
  
  const deleteTodo = useCallback((id: number) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }, [todos])
  
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed
    if (filter === 'completed') return todo.completed
    return true
  })
  
  return (
    <div className="todo-app">
      <h1>Todo List</h1>
      
      <div className="input-section">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="Add a todo..."
        />
        <button onClick={addTodo}>Add</button>
      </div>
      
      <div className="filter-section">
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('active')}>Active</button>
        <button onClick={() => setFilter('completed')}>Completed</button>
      </div>
      
      <ul className="todo-list">
        {filteredTodos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
      
      <div className="stats">
        <p>{todos.filter(t => !t.completed).length} items left</p>
      </div>
    </div>
  )
}

// Render the app
const root = document.getElementById('root')
if (root) {
  render(<TodoApp />, root)
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Keep components small and focused">
    Break down complex components into smaller, reusable pieces.
  </Accordion>

  <Accordion title="Use TypeScript for type safety">
    Define proper types for props, state, and event handlers.
  </Accordion>

  <Accordion title="Optimize with useMemo and useCallback">
    Memoize expensive computations and callbacks to prevent unnecessary re-renders.
  </Accordion>

  <Accordion title="Clean up effects properly">
    Always return cleanup functions from `useEffect` to prevent memory leaks.
  </Accordion>

  <Accordion title="Use keys for lists">
    Always provide unique keys when rendering lists to help with reconciliation.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Server Rendering" icon="server" href="/jsx/server-rendering">
    Learn about server-side JSX rendering
  </Card>

  <Card title="Streaming" icon="bolt" href="/jsx/streaming">
    Stream HTML with Suspense patterns
  </Card>

  <Card title="JSX Overview" icon="book" href="/jsx/overview">
    Back to JSX overview
  </Card>
</CardGroup>
