Server and client execution
Choose where route work runs, protect private dependencies, and keep the first browser render consistent with server HTML.
Understand the rendering model
Tavo.js renders the same TSX component model on the server and in the browser. In an SSR runtime, routes render on the server by default and hydrate in the browser. A route can opt into client-only rendering when its initial HTML is not important.
Create src/pages/browser-only.tsx
tsximport { Page } from "@tavojs/ui";
export const render = "csr";
export default function BrowserOnlyPage() {
return <Page>Rendered in the browser</Page>;
}| Stage | Route component | Universal loader | Server-only loader |
|---|---|---|---|
| Initial CSR request | Browser | Browser | Skipped |
| Initial SSR request | Server, then browser hydration | Server; result is serialized | Server; safe result is serialized |
| Later client navigation | Browser | Browser | Skipped |
| SSG build | Build process | Build process | Build process |
| ISR refresh | Server runtime | Server runtime | Server runtime |
SSR renders for the current request.
CSR sends the document shell and resolves the route in the browser.
SSG prerenders selected static routes during the build.
ISR serves cached SSR output and refreshes it after a revalidation interval.
A route
pendingexport can appear during browser route resolution, but normal SSR and static generation wait for the completed page or its error view.
Create the server module used by the example
Create src/server/session.ts before adding the account page below. The explicit server-only import and the src/server location protect the module from browser imports.
This adapter recognizes session=demo only in development to make the request boundary observable. It returns no session in production. It is a local teaching fixture, not an authentication implementation; replace it with your session provider and permission checks.
Create src/server/session.ts
tsimport "@tavojs/core/server-only";
export type Session = { accountId: string };
export type Account = { name: string };
export async function readSession(request: Request): Promise<Session | null> {
// Local example only: this cookie is not an authenticated session.
// Replace this adapter with your session provider before deployment.
if (process.env.NODE_ENV === "production") return null;
const cookie = request.headers.get("cookie") ?? "";
return /(?:^|;\s*)session=demo(?:;|$)/.test(cookie)
? { accountId: "demo" }
: null;
}
export async function getAccount(
accountId: string,
{ signal }: { signal: AbortSignal },
): Promise<Account> {
signal.throwIfAborted();
if (accountId !== "demo") throw new Error("Account not found");
return { name: "Ada" };
}
Keep private work behind a server boundary
defineServerLoader marks route data that must only resolve on the server. Tavo.js also enforces src/server/** as a server-only module boundary: the client build fails if code from that directory reaches its module graph. Put databases, sessions, private API clients, and secret-bearing code there, then reach it through server loaders, actions, middleware, or server routes.
Run this example with npm run dev:ssr. A server-only loader is skipped during browser route resolution. Its SSR result is available during initial hydration, but a later client navigation needs safe client auth state or a server endpoint such as /api/me when it must refresh that data.
Create src/pages/account.tsx
tsximport { defineServerLoader } from "@tavojs/core/router";
import { Link, Page, Text } from "@tavojs/ui";
type Account = { name: string };
export const load = defineServerLoader(async ({ request, signal }) => {
const { getAccount, readSession } = await import("../server/session");
const session = await readSession(request);
if (!session) throw new Error("Authentication required");
return getAccount(session.accountId, { signal });
});
export default function AccountPage({ data }: { data?: Account }) {
return (
<Page>
{data ? (
<Text>Welcome, {data.name}</Text>
) : (
<Text>
Account data requires a server request.{" "}
<Link as="a" href="/account">
Reload account
</Link>
.
</Text>
)}
</Page>
);
}
Keep hydration deterministic
The browser hydrates against the route data and HTML resolved by the server. A hydration warning means the initial client tree did not reproduce that output.
Read browser-only globals such as
windowandlocalStorageinonMountor another browser lifecycle callback. Atypeof windowguard prevents a crash but can still create different server and browser markup.Avoid time, randomness, and locale differences in the initial render.
Use deterministic IDs from framework helpers.
Test the production SSR build, not only the development server.