A minimal framework for React Server Components
Hono for the server, Rspack for the build, React Server Components for the rendering. One required file, nine exports, and no conventions around them.
npx @rshono/create@latest my-apppnpx @rshono/create@latest my-appyarn dlx @rshono/create@latest my-appbunx @rshono/create@latest my-app- 1 required file
- 9 exported values
- 5 direct dependencies
- 4 deploy targets
Best-in-class ergonomics: write server and client together
A page awaits your database directly, with no API layer in between. A client component reads the same url and params from one hook, and calls a ‘use server’ function like any other function. The boundary is a directive, not a directory.
src/routes.ts
Every page and endpoint in one table, matched in order.
import { defineRoutes } from '@rshono/core';
export const routes = defineRoutes({
routes: [
{ path: '/', component: () => import('./pages/home') },
{ path: '/posts/:id', component: () => import('./pages/post') },
{ type: 'endpoint', path: '/api/health', server: () => import('./api/health') },
],
notFound: { component: () => import('./pages/404') },
});Server: a page
A server component, awaiting data.
import type { PageProps } from '@rshono/core';
import { db } from '../db';
export default async function Post({ params, ctx }: PageProps<'/posts/:id'>) {
const post = await db.getPost(params.id);
const theme = ctx.cookies.get('theme') ?? 'light';
return <Article post={post} theme={theme} />;
}Client: a component
One hook for the URL, the params and the router.
'use client';
import { useNavigation } from '@rshono/core/client';
export function Pager() {
const { url, router } = useNavigation();
const page = Number(url.searchParams.get('page') ?? 1);
return <button onClick={() => router.push(`?page=${page + 1}`)}>Next</button>;
}Why choose rshono?
Because there is so little of it. Nine exported values across three import paths, five direct dependencies, and no framework-owned replacement for an HTML element you already know. Hono and Rspack stay reachable underneath, unrenamed.
One route table →
Every page and endpoint in src/routes.ts. No file-system routing, so your directory tree is yours — moving a page is an edit to one line.
The request is a prop →
A page is handed { url, params, ctx } and awaits your database directly. Nested components and actions call getRequestContext() for the same object.
One navigation hook →
useNavigation() returns the url, the params and the router. No <Link>, no <Image>, no <Head> — links are <a href>, images are <img>, forms are <form action>.
Actions, not endpoints →
A 'use server' function is callable from client code with typed arguments. Wired to a form it posts before hydration and with JavaScript disabled.
The Hono app is yours →
src/server.ts is a Hono sub-app mounted ahead of the pages, so its middleware wraps them. Endpoints, streaming, cookies, end-to-end client types.
The Rspack config too →
One hook, called once per compiler, hands you the generated config before it compiles. That is how Tailwind is wired up, and any other loader.
One toolchain, three commands
The same pair of Rspack configs builds dev and production. dev runs the real production server bundle in a worker thread, with HMR that keeps browser state.
rshono dev
rshono build
rshono startA scaffolder with six questions
Where it goes, deploy target, styling, formatter/linter preset, install, git. Every question is also a flag, a non-interactive terminal implies --yes, and the package manager that ran it is the one the project gets.
- node
- cloudflare
- vercel
- aws-lambda
Measured, not claimed
One app built three ways, on Apple M1 · 8 cores. Every metric →
Initial payload, prerendered page
60.4 kB
2.7× smaller than Next.js
Cold production build
533ms
6.6× faster than Next.js
Requests before first paint
3
3.7× fewer than Next.js
Start with a scaffold
npx @rshono/create@latest my-apppnpx @rshono/create@latest my-appyarn dlx @rshono/create@latest my-appbunx @rshono/create@latest my-app