App Routes
View as MarkdownApp routes are fully dynamic, server-side rendered (SSR) routes that hydrate on the client. They function like a Single Page Application (SPA) after the initial load.
Characteristics
- SSR: HTML is generated on the server for every request.
- Hydration: Preact hydrates the page on the client, making it fully interactive.
- Client-side Navigation: Clicking links uses client-side routing, avoiding full page reloads.
- Shared State: Layouts persist state during navigation.
The client entry
App routes hydrate on the client from a single client entry — src/main.tsx
(or src/client.tsx; .ts/.jsx/.js variants are also detected). Every
Neutron project starts with one, and the starter templates include it:
// src/main.tsx
import { init, registerRoutes } from "@neutron-build/core/client";
import { routes } from "virtual:neutron/routes";
registerRoutes(routes);
void init();
registerRoutes hands the client router the same route table the server used
(via the virtual:neutron/routes module the build injects), and init()
hydrates the server-rendered HTML and takes over client-side navigation. You
normally never edit this file — add hydration-time setup (analytics, error
reporting, a global store) before init() if you need it. If the entry is
missing, pages still server-render but do not hydrate (no interactivity or
client navigation).
Configuration
To make a route an app route, set the mode to app:
export const config = { mode: "app" };
Runtime Choice
Neutron allows you to choose the underlying runtime for your app routes.
- Preact (Default): Optimized for performance and size (~3KB runtime). Highly compatible with the React ecosystem.
- React: For apps that require specific React internals or libraries that are not fully compatible with Preact.
You can configure this in neutron.config.ts:
export default defineConfig({
runtime: "preact", // or "react"
});
Regardless of the runtime, your code uses standard React-style hooks and JSX.
Data Loading
App routes use loader functions to fetch data on the server.
import { useLoaderData } from "@neutron-build/core";
export const config = { mode: "app" };
export async function loader({ request }: LoaderArgs) {
const user = await getUser(request);
return { user };
}
export default function Profile() {
const { user } = useLoaderData<typeof loader>();
return <h1>Hello, {user.name}</h1>;
}
Mutations
App routes use action functions to handle form submissions and data mutations.
import { Form } from "@neutron-build/core";
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
// ... handle update
return { success: true };
}
export default function Settings() {
return (
<Form method="post">
<button>Update</button>
</Form>
);
}
See the Data Loading and Actions guides for more details.