安装
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill hono-jsx
复制
Hono JSX - Server-Side Rendering
Overview
Hono provides a built-in JSX renderer for server-side HTML generation. It supports async components, streaming with Suspense, and integrates seamlessly with Hono's response system.
Key Features:
Server-side JSX rendering
Async component support
Streaming with Suspense
Automatic head hoisting
Error boundaries
Context API
Zero client-side hydration overhead
When to Use This Skill
Use Hono JSX when:
Building server-rendered HTML pages
Creating email templates
Generating static HTML
Streaming large HTML responses
Building MPA (Multi-Page Applications)
Not for: Interactive SPAs (use React/Vue/Svelte instead)
Configuration
TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
Alternative: Pragma Comments
/ @jsx jsx */
/ @jsxImportSource hono/jsx */
Deno Configuration
// deno.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "npm:hono/jsx"
}
}
Basic Usage
Simple Rendering
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.html(
<html>
<head>
<title>Hello Hono</title>
</head>
<body>
Hello, World!
</body>
</html>
)
})
Components
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
// Define props type
type GreetingProps = {
name: string
age?: number
}
// Functional component
const Greeting: FC = ({ name, age }) => {
return (
Hello, {name}!
{age &&
You are {age} years old.
}
)
}
const app = new Hono()
app.get('/hello/:name', (c) => {
const name = c.req.param('name')
return c.html( )
})
Layout Components
import type { FC, PropsWithChildren } from 'hono/jsx'
const Layout: FC<PropsWithChildren<{ title: string }>> = ({ title, children }) => {
return (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
{children}
</body>
</html>
)
}
app.get('/', (c) => {
return c.html(
Welcome!
This is my home page.
)
})
Async Components
Basic Async
const AsyncUserList: FC = async () => {
const users = await fetchUsers()
return (
{users.map(user => (
{user.name}
))}
)
}
app.get('/users', async (c) => {
return c.html( )
})
Nested Async Components
const UserProfile: FC<{ id: string }> = async ({ id }) => {
const user = await fetchUser(id)
return (
)
}
const UserPosts: FC<{ userId: string }> = async ({ userId }) => {
const posts = await fetchUserPosts(userId)
return (
Posts
{posts.map(post => (
{post.title}
{post.excerpt}
))}
)
}
Streaming with Suspense
Basic Streaming
import { Suspense, renderToReadableStream } from 'hono/jsx/streaming'
const SlowComponent: FC = async () => {
await new Promise(resolve => setTimeout(resolve, 2000))
return
Loaded after 2 seconds!
}
app.get('/stream', (c) => {
const stream = renderToReadableStream(
<html>
<body>
Streaming Demo
Loading...\ }>
</body>
</html>
)
return c.body(stream, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'Transfer-Encoding': 'chunked'
}
})
})
Multiple Suspense Boundaries
const Page: FC = () => {
return (
Dashboard
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Loading stats...</div>}>
<Statistics />
</Suspense>
<Suspense fallback={<div>Loading feed...</div>}>
<ActivityFeed />
</Suspense>
</Layout>
)
}
Error Boundaries
import { ErrorBoundary } from 'hono/jsx'
const RiskyComponent: FC = () => {
if (Math.random() > 0.5) {
throw new Error('Random error!')
}
return
Success!
}
const ErrorFallback: FC<{ error: Error }> = ({ error }) => {
return (
Something went wrong
{error.message}
)
}
app.get('/risky', (c) => {
return c.html(
)
})
Async Error Boundaries
const AsyncRiskyComponent: FC = async () => {
const data = await fetchData()
if (!data) {
throw new Error('Data not found')
}
return
{data}
}
// Error boundary catches async errors too
Error: {error.message}
}>
Context API
Creating Context
import { createContext, useContext } from 'hono/jsx'
type Theme = 'light' | 'dark'
const ThemeContext = createContext('light')
const ThemedButton: FC<{ label: string }> = ({ label }) => {
const theme = useContext(ThemeContext)
const className = theme === 'dark' ? 'btn-dark' : 'btn-light'
return {label}
}
const App: FC<{ theme: Theme }> = ({ theme, children }) => {
return (
app theme-${theme}}>
{children}
)
}
app.get('/', (c) => {
const theme = c.req.query('theme') as Theme || 'light'
return c.html(
)
})
Head Hoisting
Tags like <title>, <meta>, <link>, and <script> are automatically hoisted to <head>:
const Page: FC<{ title: string }> = ({ title, children }) => {
return (
<html>
<head>
{/ Base head content /}
</head>
<body>
{/ These will be hoisted to head! /}
<title>{title}</title>
<meta name="description" content="Page description" />
<link rel="stylesheet" href="/page.css" />
<div>{children}</div>
</body>
</html>
)
}
// Even from nested components
const SEO: FC<{ title: string; description: string }> = ({ title, description }) => {
return (
<>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
</>
)
}
const Article: FC<{ article: Article }> = ({ article }) => {
return (
{article.title}
{article.content}
)
}
Raw HTML
dangerouslySetInnerHTML
const RawHtml: FC<{ html: string }> = ({ html }) => {
return
}
// Usage
const markdown = await renderMarkdown(content)
Raw Helper
import { raw } from 'hono/html'
const Page: FC = () => {
return (
<html>
<body>
{raw('<script>console.log("Hello")</script>')}
</body>
</html>
)
}
Fragments
import { Fragment } from 'hono/jsx'
// Using Fragment
const List: FC = () => {
return (
Item 1
Item 2
Item 3
)
}
// Using short syntax
const List2: FC = () => {
return (
<>
Item 1
Item 2
Item 3
</>
)
}
Memoization
import { memo } from 'hono/jsx'
// Expensive to compute
const ExpensiveComponent: FC<{ data: string[] }> = ({ data }) => {
const processed = data.map(item => item.toUpperCase()).join(', ')
return
{processed}
}
// Memoize the result
const MemoizedExpensive = memo(ExpensiveComponent)
// Won't recompute if data is the same
Integration Patterns
With HTMX
const TodoList: FC<{ todos: Todo[] }> = ({ todos }) => {
return (
{todos.map(todo => (
{todo.text}
/todos/${todo.id}}
hx-target="closest li"
hx-swap="outerHTML"
>
Delete
))}
)
}
app.get('/todos', async (c) => {
const todos = await getTodos()
return c.html(
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
Todos
)
})
app.post('/todos', async (c) => {
const { text } = await c.req.parseBody()
const todo = await createTodo(text as string)
return c.html(
{todo.text}
/todos/${todo.id}}
hx-target="closest li"
hx-swap="outerHTML"
>
Delete
)
})
With Tailwind CSS
const Button: FC<{ variant: 'primary' | 'secondary' }> = ({ variant, children }) => {
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors'
const variantClasses = variant === 'primary'
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
return (
${baseClasses} ${variantClasses}}>
{children}
)
}
Quick Reference
Key Imports
import type { FC, PropsWithChildren } from 'hono/jsx'
import { Fragment, createContext, useContext, memo } from 'hono/jsx'
import { Suspense, renderToReadableStream } from 'hono/jsx/streaming'
import { ErrorBoundary } from 'hono/jsx'
import { raw } from 'hono/html'
Response Methods
// Direct render
c.html( )
// Streaming
c.body(renderToReadableStream( ), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' }
})
Component Types
// Basic
const Comp: FC = () =>
Hello
// With props
const Comp: FC<{ name: string }> = ({ name }) =>
{name}
// With children
const Comp: FC = ({ children }) => {children}
// Async
const Comp: FC = async () => {
const data = await fetch()
return
{data}
}
Related Skills
hono-core - Framework fundamentals
hono-middleware - Middleware patterns
hono-cloudflare - Edge deployment
Version: Hono 4.x Last Updated: January 2025 License: MIT
← 返回排行榜