Skip to main content
Hono’s server-side JSX rendering (hono/jsx) converts JSX components directly to HTML strings. This approach is optimized for server performance and integrates seamlessly with Hono’s request/response model.

Setup

Configure your tsconfig.json for server-side JSX:
tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

Using c.html() with JSX

The c.html() method accepts JSX and returns an HTML response:
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.html(
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <h1>Hello, Hono JSX!</h1>
      </body>
    </html>
  )
})

export default app

Functional Components

Create reusable components as functions:
import type { FC } from 'hono/jsx'

type PageProps = {
  title: string
  description?: string
}

const Page: FC<PageProps> = ({ title, description, children }) => {
  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{title}</title>
        {description && <meta name="description" content={description} />}
      </head>
      <body>
        <div className="container">{children}</div>
      </body>
    </html>
  )
}

app.get('/', (c) => {
  return c.html(
    <Page title="Home" description="Welcome to my site">
      <h1>Welcome!</h1>
      <p>This is the homepage.</p>
    </Page>
  )
})

Props and Type Safety

Hono JSX is fully type-safe with TypeScript:
type ButtonProps = {
  variant: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  onClick?: string // Event handler as string for server-side
  children: any
}

const Button: FC<ButtonProps> = ({ 
  variant, 
  size = 'md', 
  disabled = false,
  onClick,
  children 
}) => {
  const className = `btn btn-${variant} btn-${size}`
  
  return (
    <button 
      className={className} 
      disabled={disabled}
      onclick={onClick}
    >
      {children}
    </button>
  )
}

// Usage with type checking
app.get('/button', (c) => {
  return c.html(
    <div>
      <Button variant="primary" size="lg">
        Click me
      </Button>
      {/* TypeScript error: invalid variant */}
      {/* <Button variant="invalid">Error</Button> */}
    </div>
  )
})

Default Props

Components can have default props:
type AlertProps = {
  type: 'info' | 'warning' | 'error' | 'success'
  message: string
}

const Alert: FC<AlertProps> = ({ type, message }) => {
  const icons = {
    info: 'ℹ️',
    warning: '⚠️',
    error: '❌',
    success: '✅'
  }
  
  return (
    <div className={`alert alert-${type}`}>
      <span className="icon">{icons[type]}</span>
      <span className="message">{message}</span>
    </div>
  )
}

// Set default props
Alert.defaultProps = {
  type: 'info'
}
Source: src/jsx/base.ts:17-21

Context API

Share data across components without prop drilling:
import { createContext, useContext } from 'hono/jsx'
import type { FC } from 'hono/jsx'

type Theme = 'light' | 'dark'

// Create a context
const ThemeContext = createContext<Theme>('light')

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

const ThemedCard: FC = ({ children }) => {
  const theme = useContext(ThemeContext)
  
  return (
    <div className={`card-${theme}`}>
      {children}
    </div>
  )
}

app.get('/themed', (c) => {
  return c.html(
    <ThemeContext.Provider value="dark">
      <ThemedCard>
        <h2>Dark Theme Card</h2>
        <ThemedButton>Dark Button</ThemedButton>
      </ThemedCard>
    </ThemeContext.Provider>
  )
})
Source: src/jsx/context.ts:15-50

Memo for Performance

Memoize components to avoid re-rendering:
import { memo } from 'hono/jsx'
import type { FC } from 'hono/jsx'

type ExpensiveProps = {
  data: number[]
  multiplier: number
}

const ExpensiveComponent: FC<ExpensiveProps> = ({ data, multiplier }) => {
  const result = data.reduce((sum, n) => sum + n * multiplier, 0)
  
  return <div>Result: {result}</div>
}

// Memoize the component
const MemoizedExpensive = memo(ExpensiveComponent)

// Custom comparison function
const MemoizedWithCustomCompare = memo(
  ExpensiveComponent,
  (prevProps, nextProps) => {
    return prevProps.multiplier === nextProps.multiplier &&
           prevProps.data.length === nextProps.data.length
  }
)
Source: src/jsx/base.ts:381-402

Cloning Elements

Clone and modify existing JSX elements:
import { cloneElement } from 'hono/jsx'

const AddProps: FC<{ children: any }> = ({ children }) => {
  return cloneElement(children, { 
    className: 'enhanced',
    'data-enhanced': 'true' 
  })
}

app.get('/clone', (c) => {
  return c.html(
    <AddProps>
      <div>This div will have additional props</div>
    </AddProps>
  )
})
Source: src/jsx/base.ts:423-440

Children Utilities

Manipulate children with the Children API:
import { Children } from 'hono/jsx'

const List: FC = ({ children }) => {
  const items = Children.toArray(children)
  
  return (
    <ul>
      {items.map((child, index) => (
        <li key={index}>
          {child}
        </li>
      ))}
    </ul>
  )
}

app.get('/list', (c) => {
  return c.html(
    <List>
      <span>Item 1</span>
      <span>Item 2</span>
      <span>Item 3</span>
    </List>
  )
})

Handling Events (Server-Side)

On the server, event handlers are rendered as string attributes:
const InteractiveButton = () => {
  return (
    <button onclick="alert('Hello!')">
      Click me
    </button>
  )
}

const FormWithHandler = () => {
  return (
    <form 
      method="POST" 
      action="/submit"
      onsubmit="return confirm('Submit form?')"
    >
      <input type="text" name="username" />
      <button type="submit">Submit</button>
    </form>
  )
}
For interactive client-side handlers, use hono/jsx/dom for client-side rendering.

SVG Support

Render SVG elements with proper namespace handling:
const Icon = () => {
  return (
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
      <path 
        d="M12 2L2 7l10 5 10-5-10-5z" 
        fill="currentColor"
        strokeWidth="2"
      />
      <path 
        d="M2 17l10 5 10-5M2 12l10 5 10-5" 
        stroke="currentColor"
        strokeWidth="2"
      />
    </svg>
  )
}
Source: src/jsx/base.ts:335-345

Async Components

Components can be async for data fetching:
type Post = {
  id: number
  title: string
  body: string
}

const fetchPost = async (id: number): Promise<Post> => {
  const res = await fetch(`https://api.example.com/posts/${id}`)
  return res.json()
}

const PostDetail: FC<{ id: number }> = async ({ id }) => {
  const post = await fetchPost(id)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  )
}

app.get('/post/:id', async (c) => {
  const id = Number(c.req.param('id'))
  
  return c.html(
    <div>
      <PostDetail id={id} />
    </div>
  )
})
For streaming async components, see the Streaming documentation.

Raw HTML

Insert raw HTML strings:
import { raw } from 'hono/helper/html'

const RawContent = () => {
  const htmlContent = '<strong>Bold text</strong>'
  
  return (
    <div>
      {raw(htmlContent)}
    </div>
  )
}

Boolean Attributes

Hono automatically handles boolean attributes:
const FormFields = () => {
  return (
    <form>
      <input type="text" required />  {/* renders as required="" */}
      <input type="checkbox" checked={true} />  {/* renders as checked="" */}
      <input type="text" disabled={false} />  {/* disabled not rendered */}
      <button type="submit" autofocus>Submit</button>
    </form>
  )
}
Source: src/jsx/base.ts:68-95

Error Boundaries

Handle errors gracefully:
import { ErrorBoundary } from 'hono/jsx'

const ProblematicComponent = () => {
  throw new Error('Something went wrong!')
  return <div>This won't render</div>
}

const Fallback = () => {
  return <div>An error occurred. Please try again later.</div>
}

app.get('/with-error', (c) => {
  return c.html(
    <ErrorBoundary fallback={<Fallback />}>
      <ProblematicComponent />
    </ErrorBoundary>
  )
})
Source: src/jsx/components.ts:52-231

Layout Composition

Build complex layouts with component composition:
const Header: FC = () => (
  <header>
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/contact">Contact</a>
    </nav>
  </header>
)

const Footer: FC = () => (
  <footer>
    <p>&copy; 2024 My Company</p>
  </footer>
)

const Layout: FC<{ title: string }> = ({ title, children }) => (
  <html>
    <head>
      <title>{title}</title>
      <link rel="stylesheet" href="/styles.css" />
    </head>
    <body>
      <Header />
      <main>{children}</main>
      <Footer />
    </body>
  </html>
)

app.get('/', (c) => {
  return c.html(
    <Layout title="Home">
      <h1>Welcome to my site</h1>
      <p>Content goes here</p>
    </Layout>
  )
})

app.get('/about', (c) => {
  return c.html(
    <Layout title="About">
      <h1>About Us</h1>
      <p>Learn more about our company</p>
    </Layout>
  )
})

Best Practices

Define proper types for all component props to catch errors at compile time.
type Props = {
  name: string
  age: number
}

const User: FC<Props> = ({ name, age }) => {
  return <div>{name} is {age} years old</div>
}
Components should be pure functions that return consistent output for the same input.
// Good: Pure component
const Greeting = ({ name }: { name: string }) => {
  return <h1>Hello, {name}!</h1>
}

// Avoid: Side effects in render
const BadComponent = () => {
  console.log('Rendering...') // Side effect
  return <div>Content</div>
}
Break down complex UIs into smaller, reusable components.
// Instead of one large component
const Page = () => (
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
)

// Extract into smaller components
const Header = () => <header>...</header>
const Main = () => <main>...</main>
const Footer = () => <footer>...</footer>

const Page = () => (
  <div>
    <Header />
    <Main />
    <Footer />
  </div>
)
Context is great for global data, but overusing it can make components less reusable.
// Good: Use context for truly global data
const ThemeContext = createContext('light')

// Avoid: Passing down local state via context
// Instead, use props for component-specific data

Next Steps

DOM Rendering

Learn about client-side rendering with hono/jsx/dom

Streaming

Stream HTML responses with Suspense

HTML Helper

Use the html helper for template rendering

Middleware

Use the JSX renderer middleware

Build docs developers (and LLMs) love