Quick Start

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".

getLoaderData is 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