Quick Start
Build a movies list with a server loader and a form action — the full file pair pattern in one example.
Prerequisites
Clone the starter and install dependencies:
git clone <repo-url> my-app
cd my-app
pnpm install
pnpm dev
Open http://localhost:5173. The dev server runs both the Hono server and the Vite HMR client. server.tsx handles API routes and the catch-all SSR handler; iso.tsx defines all client-side routes. Both /__loaders and /__actions endpoints are already registered — any .server.ts file you add to src/pages/ is automatically discovered.
1. Create a page
Create src/pages/movies.tsx:
import type { FunctionComponent } from 'preact';
import { getLoaderData } from '@hono-preact/iso';
const Movies: FunctionComponent = () => {
return (
<main>
<h1>Movies</h1>
</main>
);
};
Movies.displayName = 'Movies';
export default getLoaderData(Movies);
Register the route in src/iso.tsx alongside the other lazy imports:
const Movies = lazy(() => import('./pages/movies.js'));
// inside <Router>:
<Route path="/movies" component={Movies} />
Open http://localhost:5173/movies. You should see "Movies".
getLoaderDatais always required, even for pages with no data. It wires the component into the loader/preload system so hydration works correctly.
2. Add a server loader
Create src/pages/movies.server.ts:
import { type Loader } from '@hono-preact/iso';
export type Movie = { id: string; title: string };
const store: Movie[] = [
{ id: '1', title: 'The Godfather' },
{ id: '2', title: 'Chinatown' },
];
const getMovies = () => store;
const addMovieToStore = (title: string) =>
store.push({ id: String(store.length + 1), title });
const serverLoader: Loader<{ movies: Movie[] }> = async () => ({
movies: getMovies(),
});
export default serverLoader;
Update src/pages/movies.tsx to use the loader data:
import type { FunctionComponent } from 'preact';
import { getLoaderData, type LoaderData } from '@hono-preact/iso';
import serverLoader, { type Movie } from './movies.server.js';
const Movies: FunctionComponent<LoaderData<{ movies: Movie[] }>> = ({ loaderData }) => {
return (
<main>
<h1>Movies</h1>
<ul>
{loaderData?.movies.map((m) => (
<li key={m.id}>{m.title}</li>
))}
</ul>
</main>
);
};
Movies.displayName = 'Movies';
export default getLoaderData(Movies, { serverLoader });
Reload http://localhost:5173/movies. The list renders server-side on first load — the data is preloaded into the HTML. On client-side navigation away and back, the framework calls the loader over RPC. Same function, no manual wiring.
3. Add a server action
Add serverActions to src/pages/movies.server.ts:
import { defineAction, type Loader } from '@hono-preact/iso';
export type Movie = { id: string; title: string };
const store: Movie[] = [
{ id: '1', title: 'The Godfather' },
{ id: '2', title: 'Chinatown' },
];
const getMovies = () => store;
const addMovieToStore = (title: string) =>
store.push({ id: String(store.length + 1), title });
const serverLoader: Loader<{ movies: Movie[] }> = async () => ({
movies: getMovies(),
});
export default serverLoader;
export const serverActions = {
addMovie: defineAction<{ title: string }, { ok: boolean }>(
async (_ctx, { title }) => {
addMovieToStore(title);
return { ok: true };
}
),
};
Update src/pages/movies.tsx to add the form:
import type { FunctionComponent } from 'preact';
import { Form, getLoaderData, type LoaderData } from '@hono-preact/iso';
import serverLoader, { serverActions, type Movie } from './movies.server.js';
const Movies: FunctionComponent<LoaderData<{ movies: Movie[] }>> = ({ loaderData }) => {
return (
<main>
<h1>Movies</h1>
<Form action={serverActions.addMovie} invalidate="auto">
<input name="title" placeholder="Movie title" required />
<button type="submit">Add</button>
</Form>
<ul>
{loaderData?.movies.map((m) => (
<li key={m.id}>{m.title}</li>
))}
</ul>
</main>
);
};
Movies.displayName = 'Movies';
export default getLoaderData(Movies, { serverLoader });
Submit a title in the form. The action runs on the server, invalidate="auto" re-fetches the loader, and the list updates — no manual state management.
What's next
- Server Loaders — caching, named caches, path params
- Server Actions —
useAction, optimistic updates, file uploads, streaming - Route Guards — protect pages with server and client guard chains
- Loading States — show a fallback while the loader fetches
- Build & Deploy — production build and deployment