hono-jsx
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHono JSX Skill
Hono JSX Skill
Build UI with : server-rendered HTML first, a small amount of client-side JSX only where the browser needs state. The skill covers the JSX syntax and ; this skill covers how to structure the app. Cloudflare Workers with Vite is the default setup; see "Other runtimes" for the rest.
hono/jsxhonoc.html()使用 构建 UI:优先采用服务端渲染 HTML,仅在浏览器需要状态管理的地方使用少量客户端 JSX。 skill 涵盖 JSX 语法和 ;本 skill 讲解如何组织应用。默认环境是结合 Vite 的 Cloudflare Workers;其余环境请参阅“其他运行时”。
hono/jsxhonoc.html()No React
不使用 React
hono/jsx- Do not install ,
react,react-dom, or@types/react. Do not import from@vitejs/plugin-react.react - has
tsconfig.jsonand"jsx": "react-jsx". The"jsxImportSource": "hono/jsx"value is the transform name, not a React dependency.react-jsx - Server components are plain functions rendered once per request. No hooks, no state, no event handlers on the server. Need data: make the component and
async. Need interactivity: a client component withawait, for that part only.hono/jsx/dom - Write , not
class.className - exports both flavors: import from
vite-ssr-components, never fromvite-ssr-components/hono./react
hono/jsx- 不要安装 、
react、react-dom或@types/react。不要从@vitejs/plugin-react导入内容。react - 中包含
tsconfig.json和"jsx": "react-jsx"。"jsxImportSource": "hono/jsx"是转换器名称,并不表示依赖 React。react-jsx - 服务端组件是普通函数,每次请求渲染一次。服务端没有 Hooks、state 或 event handlers。需要数据时,将组件设为 并使用
async。需要交互性时,仅为对应部分使用基于await的客户端组件。hono/jsx/dom - 使用 ,不要使用
class。className - 同时导出两种形式:请从
vite-ssr-components导入,绝不要从vite-ssr-components/hono导入。/react
Project Layout (Cloudflare Workers + Vite)
项目布局(Cloudflare Workers + Vite)
This is what generates with the template. Match it in existing projects instead of inventing a different structure.
create-honocloudflare-workers+vitesrc/
index.tsx # Hono app, routes
renderer.tsx # jsxRenderer layout: <html>, <head>, Vite assets
client.ts # optional: browser-side code
style.css
public/ # static files served as-is
vite.config.ts
wrangler.jsoncjsonc
// package.json (scripts)
{
"dev": "vite",
"build": "vite build",
"preview": "$npm_execpath run build && vite preview",
"deploy": "$npm_execpath run build && wrangler deploy",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
}ts
// vite.config.ts
import { cloudflare } from '@cloudflare/vite-plugin'
import { defineConfig } from 'vite'
import ssrPlugin from 'vite-ssr-components/plugin'
export default defineConfig({
plugins: [cloudflare(), ssrPlugin()],
})tsx
// src/renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Link, Script, ViteClient } from 'vite-ssr-components/hono'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
<ViteClient />
<Link href="/src/style.css" rel="stylesheet" />
<Script src="/src/client.ts" />
</head>
<body>{children}</body>
</html>
)
})tsx
// src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer'
const app = new Hono<{ Bindings: CloudflareBindings }>()
app.use(renderer)
app.get('/', (c) => {
return c.render(<h1>Hello!</h1>)
})
export default appvite-ssr-components<ViteClient /><Script><Link>rollupOptions.input<Script><Link>componentsDev server is (Vite with the Cloudflare plugin, so bindings are real). Bindings types come from ; never hand-write them.
npm run devc.envnpm run cf-typegen这是 使用 模板生成的结构。现有项目应与该结构保持一致,不要另创一套布局。
create-honocloudflare-workers+vitesrc/
index.tsx # Hono app, routes
renderer.tsx # jsxRenderer layout: <html>, <head>, Vite assets
client.ts # optional: browser-side code
style.css
public/ # static files served as-is
vite.config.ts
wrangler.jsoncjsonc
// package.json (scripts)
{
"dev": "vite",
"build": "vite build",
"preview": "$npm_execpath run build && vite preview",
"deploy": "$npm_execpath run build && wrangler deploy",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
}ts
// vite.config.ts
import { cloudflare } from '@cloudflare/vite-plugin'
import { defineConfig } from 'vite'
import ssrPlugin from 'vite-ssr-components/plugin'
export default defineConfig({
plugins: [cloudflare(), ssrPlugin()],
})tsx
// src/renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Link, Script, ViteClient } from 'vite-ssr-components/hono'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
<ViteClient />
<Link href="/src/style.css" rel="stylesheet" />
<Script src="/src/client.ts" />
</head>
<body>{children}</body>
</html>
)
})tsx
// src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer'
const app = new Hono<{ Bindings: CloudflareBindings }>()
app.use(renderer)
app.get('/', (c) => {
return c.render(<h1>Hello!</h1>)
})
export default appvite-ssr-components<ViteClient /><Script><Link>rollupOptions.input<Script><Link>components开发服务器通过 启动(使用带 Cloudflare 插件的 Vite,因此 bindings 是真实可用的)。Bindings 类型由 生成;绝不要手动编写。
npm run devc.envnpm run cf-typegenLayouts and Pages
布局与页面
jsxRendererc.render()- Per-page head content: write ,
<title>, and<meta>inside the page component. They are hoisted into<link>. Existing head elements stay, so keep the layout's<head>out if pages set their own.<title> - Props to the layout (a title, a current-nav key): extend once, then pass the props as the second argument of
ContextRenderer.c.render()
tsx
declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, props: { title: string }): Response
}
}
export const renderer = jsxRenderer(({ children, title }) => (
<html>
<head>
<title>{title}</title>
</head>
<body>{children}</body>
</html>
))
app.get('/about', (c) => c.render(<About />, { title: 'About' }))- Nested layouts: a sub-app can register its own and receive the parent as
jsxRenderer.Layout
tsx
const blog = new Hono()
blog.use(
jsxRenderer(({ children, Layout }) => (
<Layout>
<nav>Blog</nav>
{children}
</Layout>
))
)
app.route('/blog', blog)- Request data inside components: from
useRequestContext()returnshono/jsx-renderer, so a deep component can readc,c.req, orc.envwithout prop drilling. Prefer loading data in the handler and passing props when the data is only needed at one place.c.var
tsx
import { useRequestContext } from 'hono/jsx-renderer'
const UserMenu = async () => {
const c = useRequestContext<{ Bindings: CloudflareBindings }>()
const user = await c.env.KV.get('user')
return <span>{user}</span>
}- Shared components: put reusable pieces in . They are functions taking props;
src/components/andFC<Props>fromPropsWithChildren<Props>type them.hono/jsx
jsxRendererc.render()- 每页的 head 内容:在页面组件内写入 、
<title>和<meta>。它们会被提取到<link>中。已有的 head 元素会保留,因此如果页面自行设置<head>,请不要在布局中保留<title>。<title> - 向布局传递属性(标题、当前导航项等):扩展一次 ,然后将这些属性作为
ContextRenderer的第二个参数传入。c.render()
tsx
declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, props: { title: string }): Response
}
}
export const renderer = jsxRenderer(({ children, title }) => (
<html>
<head>
<title>{title}</title>
</head>
<body>{children}</body>
</html>
))
app.get('/about', (c) => c.render(<About />, { title: 'About' }))- 嵌套布局:子应用可以注册自己的 ,并通过
jsxRenderer接收父布局。Layout
tsx
const blog = new Hono()
blog.use(
jsxRenderer(({ children, Layout }) => (
<Layout>
<nav>Blog</nav>
{children}
</Layout>
))
)
app.route('/blog', blog)- 组件内访问请求数据:中的
hono/jsx-renderer会返回useRequestContext(),因此深层组件无需逐层传递 props 即可读取c、c.req或c.env。如果数据只在一处使用,优先在 handler 中加载数据并通过 props 传递。c.var
tsx
import { useRequestContext } from 'hono/jsx-renderer'
const UserMenu = async () => {
const c = useRequestContext<{ Bindings: CloudflareBindings }>()
const user = await c.env.KV.get('user')
return <span>{user}</span>
}- 共享组件:将可复用部分放在 中。它们是接收 props 的函数;可使用
src/components/中的hono/jsx和FC<Props>进行类型标注。PropsWithChildren<Props>
Forms and Mutations
表单与数据变更
The default is an HTML form posting to a Hono route and a redirect. No client JavaScript is required, and it works before any script loads.
tsx
app.get('/todos', async (c) => {
const todos = await listTodos(c.env)
return c.render(
<>
<form method="post" action="/todos">
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>{todos.map((t) => <li>{t.title}</li>)}</ul>
</>
)
})
app.post('/todos', async (c) => {
const body = await c.req.parseBody()
await addTodo(c.env, String(body.title))
return c.redirect('/todos')
})Validate with or a schema validator from the skill; on failure re-render the page with the error message rather than returning JSON.
validator('form', ...)hono默认方案是使用 HTML form 将数据提交到 Hono route,然后执行重定向。这样无需客户端 JavaScript,并且在任何脚本加载前都能正常工作。
tsx
app.get('/todos', async (c) => {
const todos = await listTodos(c.env)
return c.render(
<>
<form method="post" action="/todos">
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>{todos.map((t) => <li>{t.title}</li>)}</ul>
</>
)
})
app.post('/todos', async (c) => {
const body = await c.req.parseBody()
await addTodo(c.env, String(body.title))
return c.redirect('/todos')
})使用 或 skill 中的 schema validator 进行验证;验证失败时,重新渲染页面并显示错误消息,而不是返回 JSON。
validator('form', ...)honoClient-Side Behavior
客户端行为
Most pages need no client JavaScript. When a part does, start with plain DOM code and reach for only for UI that holds state and re-renders. Neither involves React.
hono/jsx/domPlain script — the default. Put it in and load it from the renderer with .
src/client.ts<Script src="/src/client.ts" />ts
// src/client.ts
/// <reference lib="dom" />
document.querySelector('#menu-toggle')?.addEventListener('click', () => {
document.querySelector('#menu')?.classList.toggle('open')
})hono/jsx/domrender()tsx
// src/client.tsx
/// <reference lib="dom" />
import { useState } from 'hono/jsx'
import { render } from 'hono/jsx/dom'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
render(<Counter />, document.getElementById('counter')!)Hooks (, , , ...) have React-compatible signatures and are imported from . Calling the server from the client is a to a JSON route; from adds typed calls once there is an API surface worth typing.
useStateuseEffectuseRefhono/jsxfetchhchono/clientType-checking client files. The template's has and no , so fails . Put a lib reference at the top of each client file instead of editing :
tsconfig.jsonlib: ["ESNext"]DOMdocumenttsctsconfig.jsonts
/// <reference lib="dom" />Vite does not read , so this only matters for .
libtsc大多数页面都不需要客户端 JavaScript。确实需要时,先使用原生 DOM 代码,只有需要保存状态并重新渲染的 UI 才使用 。两者都不涉及 React。
hono/jsx/dom原生脚本——默认方案。将其放在 中,并通过 renderer 中的 加载。
src/client.ts<Script src="/src/client.ts" />ts
// src/client.ts
/// <reference lib="dom" />
document.querySelector('#menu-toggle')?.addEventListener('click', () => {
document.querySelector('#menu')?.classList.toggle('open')
})hono/jsx/domrender()tsx
// src/client.tsx
/// <reference lib="dom" />
import { useState } from 'hono/jsx'
import { render } from 'hono/jsx/dom'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
render(<Counter />, document.getElementById('counter')!)Hooks(、、 等)具有与 React 兼容的签名,并从 导入。客户端调用服务器时,应对 JSON route 发起 请求;当 API surface 值得进行类型标注后,可以使用 中的 添加类型化调用。
useStateuseEffectuseRefhono/jsxfetchhono/clienthc客户端文件的类型检查。 模板的 中包含 ,没有 ,因此 无法通过 类型检查。请在每个客户端文件顶部添加 lib reference,不要修改 :
tsconfig.jsonlib: ["ESNext"]DOMdocumenttsctsconfig.jsonts
/// <reference lib="dom" />Vite 不读取 ,因此这只影响 。
libtscStyling
样式
- A stylesheet at linked with
src/style.cssin the renderer. Vite processes it, so CSS imports and PostCSS work.<Link href="/src/style.css" rel="stylesheet" /> - for component-scoped styles:
hono/cssreturns a class name,cssin<Style />emits the collected rules.<head>
tsx
import { css, Style } from 'hono/css'
const card = css`
padding: 1rem;
&:hover { background: #eee; }
`
// <head><Style /></head> ... <div class={card}>...</div>- 在 中放置样式表,并通过 renderer 中的
src/style.css引用。Vite 会处理该文件,因此 CSS imports 和 PostCSS 均可正常使用。<Link href="/src/style.css" rel="stylesheet" /> - 使用 实现组件作用域样式:
hono/css返回一个 class name,css中的<head>会输出收集到的规则。<Style />
tsx
import { css, Style } from 'hono/css'
const card = css`
padding: 1rem;
&:hover { background: #eee; }
`
// <head><Style /></head> ... <div class={card}>...</div>Streaming
流式渲染
For a page where one part is slow, keep the shell instant: enable on the renderer and wrap the slow async component in .
stream: trueSuspensetsx
import { Suspense } from 'hono/jsx/streaming'
export const renderer = jsxRenderer(({ children }) => <html><body>{children}</body></html>, { stream: true })
app.get('/', (c) =>
c.render(
<Suspense fallback={<p>Loading...</p>}>
<SlowList />
</Suspense>
)
)SuspenseErrorBoundary对于某个部分加载较慢的页面,应保持页面 shell 即时呈现:在 renderer 上启用 ,并使用 包裹速度较慢的异步组件。
stream: trueSuspensetsx
import { Suspense } from 'hono/jsx/streaming'
export const renderer = jsxRenderer(({ children }) => <html><body>{children}</body></html>, { stream: true })
app.get('/', (c) =>
c.render(
<Suspense fallback={<p>Loading...</p>}>
<SlowList />
</Suspense>
)
)文档中将 和 标记为 experimental。仅在数据确实较慢时使用,不要默认启用。
SuspenseErrorBoundaryEscaping and Raw HTML
转义与原始 HTML
JSX escapes interpolated strings. To insert trusted HTML, use or from . Never pass user input through either.
dangerouslySetInnerHTML={{ __html }}raw()hono/htmlJSX 会转义插入的字符串。要插入受信任的 HTML,请使用 或 中的 。绝不要将用户输入传给二者中的任何一个。
dangerouslySetInnerHTML={{ __html }}hono/htmlraw()Other Runtimes
其他运行时
Without the Cloudflare Vite plugin, use for dev and for the build. Client assets are then built in a separate Vite mode and referenced by fixed paths:
@hono/vite-dev-server@hono/vite-build/<runtime>ts
// vite.config.ts
import build from '@hono/vite-build/node' // or /bun, /cloudflare-workers
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
if (mode === 'client') {
return {
build: { rollupOptions: { input: './src/client.tsx', output: { entryFileNames: 'static/client.js' } } },
}
}
return {
plugins: [
// staticRoot is a Node adapter option: it serves dist/static from the built server
build({ entry: 'src/index.tsx', staticRoot: './dist' }),
devServer({ entry: 'src/index.tsx' }),
],
}
})tsx
// renderer: pick the script path by mode
{import.meta.env.PROD ? (
<script type="module" src="/static/client.js" />
) : (
<script type="module" src="/src/client.tsx" />
)}Build with , then run (the Node build bundles , install it as a dependency). also has adapters (, , ) when bindings or runtime APIs are needed in dev.
vite build --mode client && vite buildnode dist/index.js@hono/node-server@hono/vite-dev-server/cloudflare/node/bun不使用 Cloudflare Vite 插件时,开发使用 ,构建使用 。此时客户端资源通过单独的 Vite mode 构建,并使用固定路径引用:
@hono/vite-dev-server@hono/vite-build/<runtime>ts
// vite.config.ts
import build from '@hono/vite-build/node' // or /bun, /cloudflare-workers
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
if (mode === 'client') {
return {
build: { rollupOptions: { input: './src/client.tsx', output: { entryFileNames: 'static/client.js' } } },
}
}
return {
plugins: [
// staticRoot is a Node adapter option: it serves dist/static from the built server
build({ entry: 'src/index.tsx', staticRoot: './dist' }),
devServer({ entry: 'src/index.tsx' }),
],
}
})tsx
// renderer: pick the script path by mode
{import.meta.env.PROD ? (
<script type="module" src="/static/client.js" />
) : (
<script type="module" src="/src/client.tsx" />
)}执行 ,然后运行 。Node 构建会打包 ,请将其作为依赖安装。当开发环境需要 bindings 或 runtime API 时, 也提供 adapters(、、)。
vite build --mode client && vite buildnode dist/index.js@hono/node-server@hono/vite-dev-server/cloudflare/node/bunDocumentation
文档
Fetch with (see the skill):
Accept: text/markdownhono使用 获取文档(参见 skill):
Accept: text/markdownhonoChecklist
检查清单
- No React packages; is
jsxImportSource;hono/jsxattributes.class - Layout in , pages via
jsxRenderer, per-pagec.render()inside the page.<title> - Forms post to routes and redirect; client JavaScript only where needed, plain DOM first.
- Vite assets go through (
vite-ssr-components,ViteClient,Script), not hand-writtenLinktags, on Cloudflare.<script> - Bindings types from .
npm run cf-typegen
- 不使用 React 包;为
jsxImportSource;使用hono/jsx属性。class - 在 中定义布局,通过
jsxRenderer渲染页面,每页的c.render()写在页面内部。<title> - 表单提交到 route 后执行重定向;仅在必要位置使用客户端 JavaScript,并优先采用原生 DOM。
- 在 Cloudflare 上,Vite 资源应通过 (
vite-ssr-components、ViteClient、Script)处理,不要手写Link标签。<script> - Bindings 类型由 生成。
npm run cf-typegen