Claude
Skills
Sign in
Back

hono-jsx

Included with Lifetime
$97 forever

Hono JSX - server-side rendering, streaming, async components, and HTML generation patterns

Web Devhonojsxssrstreaminghtmlcomponentssuspense

What this skill does


# 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

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

### Alternative: Pragma Comments

```tsx
/** @jsx jsx */
/** @jsxImportSource hono/jsx */
```

### Deno Configuration

```json
// deno.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "npm:hono/jsx"
  }
}
```

## Basic Usage

### Simple Rendering

```tsx
import { Hono } from 'hono'

const app = new Hono()

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

### Components

```tsx
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'

// Define props type
type GreetingProps = {
  name: string
  age?: number
}

// Functional component
const Greeting: FC<GreetingProps> = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old.</p>}
    </div>
  )
}

const app = new Hono()

app.get('/hello/:name', (c) => {
  const name = c.req.param('name')
  return c.html(<Greeting name={name} />)
})
```

### Layout Components

```tsx
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>
        <header>
          <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>&copy; 2025 My App</p>
        </footer>
      </body>
    </html>
  )
}

app.get('/', (c) => {
  return c.html(
    <Layout title="Home">
      <h1>Welcome!</h1>
      <p>This is my home page.</p>
    </Layout>
  )
})
```

## Async Components

### Basic Async

```tsx
const AsyncUserList: FC = async () => {
  const users = await fetchUsers()

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

app.get('/users', async (c) => {
  return c.html(<AsyncUserList />)
})
```

### Nested Async Components

```tsx
const UserProfile: FC<{ id: string }> = async ({ id }) => {
  const user = await fetchUser(id)

  return (
    <div class="profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <UserPosts userId={id} />
    </div>
  )
}

const UserPosts: FC<{ userId: string }> = async ({ userId }) => {
  const posts = await fetchUserPosts(userId)

  return (
    <div class="posts">
      <h3>Posts</h3>
      {posts.map(post => (
        <article key={post.id}>
          <h4>{post.title}</h4>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}
```

## Streaming with Suspense

### Basic Streaming

```tsx
import { Suspense, renderToReadableStream } from 'hono/jsx/streaming'

const SlowComponent: FC = async () => {
  await new Promise(resolve => setTimeout(resolve, 2000))
  return <div>Loaded after 2 seconds!</div>
}

app.get('/stream', (c) => {
  const stream = renderToReadableStream(
    <html>
      <body>
        <h1>Streaming Demo</h1>
        <Suspense fallback={<div>Loading...</div>}>
          <SlowComponent />
        </Suspense>
      </body>
    </html>
  )

  return c.body(stream, {
    headers: {
      'Content-Type': 'text/html; charset=UTF-8',
      'Transfer-Encoding': 'chunked'
    }
  })
})
```

### Multiple Suspense Boundaries

```tsx
const Page: FC = () => {
  return (
    <Layout title="Dashboard">
      <h1>Dashboard</h1>

      <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

```tsx
import { ErrorBoundary } from 'hono/jsx'

const RiskyComponent: FC = () => {
  if (Math.random() > 0.5) {
    throw new Error('Random error!')
  }
  return <div>Success!</div>
}

const ErrorFallback: FC<{ error: Error }> = ({ error }) => {
  return (
    <div class="error">
      <h3>Something went wrong</h3>
      <p>{error.message}</p>
    </div>
  )
}

app.get('/risky', (c) => {
  return c.html(
    <Layout title="Risky Page">
      <ErrorBoundary fallback={ErrorFallback}>
        <RiskyComponent />
      </ErrorBoundary>
    </Layout>
  )
})
```

### Async Error Boundaries

```tsx
const AsyncRiskyComponent: FC = async () => {
  const data = await fetchData()

  if (!data) {
    throw new Error('Data not found')
  }

  return <div>{data}</div>
}

// Error boundary catches async errors too
<ErrorBoundary fallback={({ error }) => <p>Error: {error.message}</p>}>
  <AsyncRiskyComponent />
</ErrorBoundary>
```

## Context API

### Creating Context

```tsx
import { createContext, useContext } from 'hono/jsx'

type Theme = 'light' | 'dark'

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

const ThemedButton: FC<{ label: string }> = ({ label }) => {
  const theme = useContext(ThemeContext)
  const className = theme === 'dark' ? 'btn-dark' : 'btn-light'

  return <button class={className}>{label}</button>
}

const App: FC<{ theme: Theme }> = ({ theme, children }) => {
  return (
    <ThemeContext.Provider value={theme}>
      <div class={`app theme-${theme}`}>
        {children}
      </div>
    </ThemeContext.Provider>
  )
}

app.get('/', (c) => {
  const theme = c.req.query('theme') as Theme || 'light'

  return c.html(
    <App theme={theme}>
      <ThemedButton label="Click me" />
    </App>
  )
})
```

## Head Hoisting

Tags like `<title>`, `<meta>`, `<link>`, and `<script>` are automatically hoisted to `<head>`:

```tsx
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 (
    <div>
      <SEO title={article.title} description={article.excerpt} />
      <h1>{article.title}</h1>
      <div>{article.content}</div>
    </div>
  )
}
```

## Raw HTML

### dangerouslySetInnerHTML

```tsx
const RawHtml: FC<{ html: string }> = ({ html }) => {
  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

// Usage
const markdown = await renderMarkdown(content)
<RawHtml html={markdown} />
```

### Raw Helper

```tsx
import { raw } from 'hono/ht

Related in Web Dev