hono-inertia

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Build SPA-feel apps with Inertia.js + Hono + hono/jsx

基于Inertia.js + Hono + hono/jsx构建类SPA应用

Inertia.js lets you build single-page apps without writing a separate frontend or a JSON API layer. The server returns Inertia responses (page name + props), and a thin client adapter swaps the current page component on the fly. With Hono as the server and
hono/jsx
as the client, you get SPA interactivity while staying inside one TypeScript stack — no React.
Reach for this when server-rendered
hono/jsx
is no longer enough — that is, when you need client-side state, optimistic UI, or rich forms.
Inertia.js 允许你构建单页应用,无需编写独立的前端或JSON API层。服务端返回Inertia响应(页面名称 + 属性),轻量的客户端适配器会动态替换当前页面组件。以Hono作为服务端、
hono/jsx
作为客户端,你可以在单一TypeScript技术栈内实现SPA交互——无需使用React。
当服务端渲染的
hono/jsx
无法满足需求时,就可以采用这套方案——比如当你需要客户端状态、乐观UI或复杂表单时。

Stack

技术栈

  • Server:
    hono
    +
    @hono/inertia
    . The
    inertia({ rootView })
    middleware adds
    c.render(name, props)
    for returning Inertia responses.
  • Client:
    @ts-76/inertia-hono-jsx
    . Provides
    createInertiaApp
    ,
    <Link>
    ,
    <Form>
    ,
    <Head>
    ,
    useForm
    ,
    useHttp
    , etc., on top of
    @inertiajs/core
    and
    hono/jsx/dom
    .
  • Bundling / SSR: Vite (with
    @cloudflare/vite-plugin
    when deploying to Workers) and
    vite-ssr-components
    for asset wiring. Start from the
    cloudflare-workers+vite
    Hono starter template — it has the Vite + Workers wiring already in place: https://github.com/honojs/starter/tree/main/templates/cloudflare-workers+vite.
  • Page typing:
    @hono/inertia/vite
    generates
    pages.gen.ts
    so
    PagePropsFor<Name>
    resolves to the props passed to
    c.render(Name, ...)
    .
  • 服务端:
    hono
    +
    @hono/inertia
    inertia({ rootView })
    中间件新增了
    c.render(name, props)
    方法,用于返回Inertia响应。
  • 客户端:
    @ts-76/inertia-hono-jsx
    。在
    @inertiajs/core
    hono/jsx/dom
    基础上提供了
    createInertiaApp
    <Link>
    <Form>
    <Head>
    useForm
    useHttp
    等功能。
  • 打包/SSR: Vite(部署到Workers时搭配
    @cloudflare/vite-plugin
    )和
    vite-ssr-components
    用于资源关联。可以从
    cloudflare-workers+vite
    Hono启动模板开始——它已经配置好了Vite + Workers的关联:https://github.com/honojs/starter/tree/main/templates/cloudflare-workers+vite
  • 页面类型定义:
    @hono/inertia/vite
    会生成
    pages.gen.ts
    ,因此
    PagePropsFor<Name>
    可以解析为传递给
    c.render(Name, ...)
    的属性类型。

Reference example

参考示例

yusukebe/hono-inertia-example
is the canonical layout (Hono + Inertia on Cloudflare Workers). It is written in React, so do not copy its
app/client.tsx
and
app/root-view.tsx
verbatim — those need to be rewritten against
@ts-76/inertia-hono-jsx
and
hono/jsx
. The server side (
app/server.ts
, route shape, Vite config,
wrangler.jsonc
, validation with
@hono/zod-validator
) transfers as-is.
yusukebe/hono-inertia-example
是标准的架构示例(Cloudflare Workers上的Hono + Inertia)。该示例基于React编写,因此不要直接复制其
app/client.tsx
app/root-view.tsx
——这些文件需要基于
@ts-76/inertia-hono-jsx
hono/jsx
重写。而服务端部分(
app/server.ts
、路由结构、Vite配置、
wrangler.jsonc
、使用
@hono/zod-validator
进行校验)可以直接沿用。

Server sketch

服务端示例代码

ts
// app/server.ts
import { Hono } from 'hono'
import { inertia } from '@hono/inertia'
import { rootView } from './root-view'

const app = new Hono()

app.use(inertia({ rootView }))

const routes = app
  .get('/', (c) => c.render('Home', { message: 'Hono x Inertia' }))
  .get('/users', (c) => c.render('Users/Index', { users: listUsers() }))
  .get('/users/:id{[0-9]+}', (c) => {
    const id = Number(c.req.param('id'))
    const user = findUser(id)
    if (!user) return c.notFound()
    return c.render('Users/Show', { user })
  })

export default routes
c.render(name, props)
returns either a full HTML document (on the first request) or a JSON Inertia page object (on subsequent client navigations) —
@hono/inertia
looks at the
X-Inertia
request header to decide.
ts
// app/server.ts
import { Hono } from 'hono'
import { inertia } from '@hono/inertia'
import { rootView } from './root-view'

const app = new Hono()

app.use(inertia({ rootView }))

const routes = app
  .get('/', (c) => c.render('Home', { message: 'Hono x Inertia' }))
  .get('/users', (c) => c.render('Users/Index', { users: listUsers() }))
  .get('/users/:id{[0-9]+}', (c) => {
    const id = Number(c.req.param('id'))
    const user = findUser(id)
    if (!user) return c.notFound()
    return c.render('Users/Show', { user })
  })

export default routes
c.render(name, props)
会返回完整的HTML文档(首次请求时)或JSON格式的Inertia页面对象(后续客户端导航时)——
@hono/inertia
会通过
X-Inertia
请求头来判断返回类型。

Client sketch (hono/jsx)

客户端示例代码(hono/jsx)

tsx
// app/client.tsx
import { createInertiaApp } from '@ts-76/inertia-hono-jsx'

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./pages/**/*.tsx', { eager: true })
    return pages[`./pages/${name}.tsx`]
  },
})
Without a custom
setup
, the adapter mounts
<App />
for you. It uses
hydrateRoot
(from
hono/jsx/dom
) when the root has
data-server-rendered
, and
createRoot
otherwise.
tsx
// app/client.tsx
import { createInertiaApp } from '@ts-76/inertia-hono-jsx'

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./pages/**/*.tsx', { eager: true })
    return pages[`./pages/${name}.tsx`]
  },
})
如果没有自定义
setup
,适配器会自动挂载
<App />
。当根元素带有
data-server-rendered
属性时,它会使用
hydrateRoot
(来自
hono/jsx/dom
),否则使用
createRoot

Page component (hono/jsx)

页面组件(hono/jsx)

tsx
// app/pages/Users/Index.tsx
import { Head, Link, type PageComponent } from '@ts-76/inertia-hono-jsx'

const UsersIndex: PageComponent<'Users/Index'> = ({ users }) => (
  <main>
    <Head title='Users' />
    <h1>Users</h1>
    {users.map((u) => (
      <Link href={`/users/${u.id}`} key={u.id}>
        {u.name}
      </Link>
    ))}
  </main>
)

export default UsersIndex
PageComponent<'Users/Index'>
pulls the prop type from
pages.gen.ts
, which is generated by
@hono/inertia/vite
from the server routes — so a mismatch between server and client props is a build error, not a runtime one.
tsx
// app/pages/Users/Index.tsx
import { Head, Link, type PageComponent } from '@ts-76/inertia-hono-jsx'

const UsersIndex: PageComponent<'Users/Index'> = ({ users }) => (
  <main>
    <Head title='Users' />
    <h1>Users</h1>
    {users.map((u) => (
      <Link href={`/users/${u.id}`} key={u.id}>
        {u.name}
      </Link>
    ))}
  </main>
)

export default UsersIndex
PageComponent<'Users/Index'>
会从
pages.gen.ts
中获取属性类型,该文件由
@hono/inertia/vite
根据服务端路由生成——因此服务端与客户端属性不匹配会触发构建错误,而非运行时错误。

Defaults

默认配置

  • Set
    jsxImportSource: "hono/jsx"
    in
    tsconfig.json
    .
  • Use
    vite-ssr-components/hono
    (not
    /react
    ) in
    root-view.tsx
    to inject the Vite client and asset tags.
  • Deploy with
    @cloudflare/vite-plugin
    so
    wrangler deploy
    ships the SSR worker and the client bundle together.
  • tsconfig.json
    中设置
    jsxImportSource: "hono/jsx"
  • root-view.tsx
    中使用
    vite-ssr-components/hono
    (而非
    /react
    )来注入Vite客户端和资源标签。
  • 使用
    @cloudflare/vite-plugin
    进行部署,这样
    wrangler deploy
    会将SSR Worker和客户端包一起发布。

Pitfalls

注意事项

  • @ts-76/inertia-hono-jsx
    is community-scoped and "partial"
    hono/jsx
    support per its README — browser rendering targets
    hono/jsx/dom
    , but server-side rendering fidelity is not React-grade. Verify SSR output for any non-trivial page.
  • Inertia is not an API framework. If a non-Inertia client (mobile, third-party) needs the same data, build a separate JSON endpoint — do not try to reuse the Inertia response shape.
  • Asset versioning: make sure
    X-Inertia-Version
    matches your client bundle hash, or clients silently force a full reload on every navigation.
  • On Cloudflare Workers, serve the client bundle via Workers Assets (
    @cloudflare/vite-plugin
    handles this), not via fetch-to-R2.
  • @ts-76/inertia-hono-jsx
    是社区维护的库,根据其README说明,它对
    hono/jsx
    的支持是“部分”的——浏览器渲染基于
    hono/jsx/dom
    ,但服务端渲染的保真度无法达到React级别。对于任何非简单页面,都需要验证SSR输出。
  • Inertia 不是 API框架。如果非Inertia客户端(移动端、第三方)需要相同的数据,请构建独立的JSON端点——不要尝试复用Inertia响应格式。
  • 资源版本控制:确保
    X-Inertia-Version
    与客户端包哈希值匹配,否则客户端会在每次导航时强制刷新页面。
  • 在Cloudflare Workers上,通过Workers Assets提供客户端包(
    @cloudflare/vite-plugin
    会处理此操作),不要通过fetch-to-R2的方式。

Going deeper

深入学习