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:
{
"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
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 > © 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
Use TypeScript for type safety
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 >
}
Extract reusable components
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