| name | hono-inertia |
| title | Build SPA-feel apps with Inertia.js + Hono + hono/jsx |
| description | Use @hono/inertia on the server and @ts-76/inertia-hono-jsx on the client to get SPA-style interactivity without leaving the Hono/hono-jsx stack. |
| tags | ["hono","inertia","hono-jsx","ui","spa","cloudflare-workers"] |
| references | ["https://inertiajs.com/","https://github.com/yusukebe/hono-inertia-example","https://github.com/ts-76/inertia-hono-jsx","https://www.npmjs.com/package/@hono/inertia","https://github.com/honojs/starter/tree/main/templates/cloudflare-workers+vite"] |
| related | ["hono","cloudflare"] |
Build SPA-feel apps with Inertia.js + Hono + hono/jsx
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.
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, ...).
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.
https://github.com/yusukebe/hono-inertia-example
Server sketch
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.
Client sketch (hono/jsx)
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.
Page component (hono/jsx)
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.
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.
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.
Going deeper