Edge
One source deploys to either runtime, no rewrite between them.
hono-preact v0.12.0
Hono at the edge, Preact in the browser, and one typed connection between them.
The platform
One app on Hono, built from web standards. Write it once; it renders on the server and serves realtime the same way, whether you ship to Cloudflare Workers or Node.
One source deploys to either runtime, no rewrite between them.
hono-preact is a Web Fetch app on Hono.
Loader data, actions, and route params are inferred from the server all the way to the client.
01Routing
Your routes are a data structure, not a folder tree. Nested layouts stay mounted while their child swaps, and every node owns its own data and is code-split.
defineRoutes([
{ path: '/', layout: Shell, children: [
{ path: 'projects/:id', layout: Project, children: [
{ path: 'tasks/:taskId', view: Task },
] },
] },
]);02SSR
Loaders run in parallel on the server and one HTML document streams down. The client never staircases through per-component fetches.
fetch in components
hono-preact SSR
export const serverLoaders = {
default: defineLoader(async ({ signal }) => getProjects({ signal })),
};
const View = serverLoaders.default.View(({ data }) =>
data ? <List items={data} /> : <Spinner />
);export const serverLoaders = {
default: defineLoader(async ({ signal }) => getProjects({ signal })),
};
const View = serverLoaders.default.View(({ data }) =>
data ? <List items={data} /> : <Spinner />
);03Stream
A loader can be an async generator. Each value it yields streams over SSE, or inlines during SSR, and folds into the live UI the moment it lands.
export const serverLoaders = {
feed: defineLoader(async function* ({ signal }) {
while (!signal.aborted) yield await snapshot();
}),
};
const Live = serverLoaders.feed.View(
(s) => (s.status === 'open' ? <Count n={s.data} /> : <Connecting />),
{ initial: null, reduce: (_, snap) => snap }
);export const serverLoaders = {
feed: defineLoader(async function* ({ signal }) {
while (!signal.aborted) yield await snapshot();
}),
};
const Live = serverLoaders.feed.View(
(s) => (s.status === 'open' ? <Count n={s.data} /> : <Connecting />),
{ initial: null, reduce: (_, snap) => snap }
);04Action
A mutation is a Form plus defineAction. The UI patches the instant you submit, then the server reconciles behind it. The row lands before the network settles.
const { mutate, pending } = useAction(serverActions.addTask, {
invalidate: 'auto',
onMutate: (t) => addOptimistic(t),
onError: (_e, h) => h.revert(),
});
// <Form action={serverActions.addTask}> also works with JavaScript disabledconst { mutate, pending } = useAction(serverActions.addTask, {
invalidate: 'auto',
onMutate: (t) => addOptimistic(t),
onError: (_e, h) => h.revert(),
});
// <Form action={serverActions.addTask}> also works with JavaScript disabled05Resilience
Loading, revalidating, and error are a discriminated union you match on: stale-while-revalidate and keep-last-good-value are the default, and a route error boundary contains a failure to its own pane.
serverLoaders.default.View((state) => {
switch (state.status) {
case 'loading': return <Skeleton />;
case 'revalidating': // keeps the last value
case 'success': return <List items={state.data} />;
case 'error': return <Retry onRetry={useReload().reload} />;
}
});06Navigation
Hover warms the cache before the click. hono-preact hands whole-page link prefetch to the browser-native Speculation Rules API, plus typed usePrefetch on any intent. The live docs site runs it.
// one line in your app config
export default defineApp({ speculation: true });
// or bind a specific link's loader to any intent (hover, focus, touch):
const prefetchIssue = usePrefetch(href, serverLoaders.issue);See it on the live docsWarming the Invoices route in parallel
invoices.route.jsreadyinvoices.data.jsonreadytable.cssreadychart.jsreadyInvoices
Opened from warm cache. No spinner.
07Transitions
Every client route change gets a view transition, automatically. No per-link opt-in, no keyframes to hand-write.
The tapped card grew into the page header. One shared name, no hand-written animation.
08Realtime
One typed duplex socket per client, with rooms and a presence roster. Use SSE when the server only pushes; reach for a WebSocket when the browser must talk back. On Cloudflare it fans out through one framework-provided Durable Object.
const chat = defineSocket({
data: (c) => ({ name: c.get('user').name }),
message: (s, m) => s.send({ text: `${s.data.name}: ${m.text}` }),
});
const { send, lastMessage, status } = chat.useSocket();The whole surface
A single hono-preact install gives you the runtime, /server, /vite, and both /adapter-* targets. One dependency, typed end to end, with nothing to wire up between the pieces.
import { defineRoutes } from 'hono-preact';
import { honoPreact } from 'hono-preact/vite';
import { cloudflareAdapter } from 'hono-preact/adapter-cloudflare';Ready?
You have seen the whole connection: it fetches, streams, mutates, transitions, and goes live, typed the whole way. Start with the quick start, or poke at the live demo.