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. Theinertia({ rootView })middleware addsc.render(name, props)for returning Inertia responses. - Client:
@ts-76/inertia-hono-jsx. ProvidescreateInertiaApp,<Link>,<Form>,<Head>,useForm,useHttp, etc., on top of@inertiajs/coreandhono/jsx/dom. - Bundling / SSR: Vite (with
@cloudflare/vite-pluginwhen deploying to Workers) andvite-ssr-componentsfor asset wiring. Start from thecloudflare-workers+viteHono 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/vitegeneratespages.gen.tssoPagePropsFor<Name>resolves to the props passed toc.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
// 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.
Client sketch (hono/jsx)
// 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.
Page component (hono/jsx)
// 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.
Defaults
- Set
jsxImportSource: "hono/jsx"intsconfig.json. - Use
vite-ssr-components/hono(not/react) inroot-view.tsxto inject the Vite client and asset tags. - Deploy with
@cloudflare/vite-pluginsowrangler deployships the SSR worker and the client bundle together.
Pitfalls
@ts-76/inertia-hono-jsxis community-scoped and "partial"hono/jsxsupport per its README — browser rendering targetshono/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-Versionmatches 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-pluginhandles this), not via fetch-to-R2.
Going deeper
- The Inertia protocol itself — short, worth reading in full: https://inertiajs.com/.
- The hono/jsx adapter API surface and typing model: https://github.com/ts-76/inertia-hono-jsx.
- The example layout to copy: https://github.com/yusukebe/hono-inertia-example.