Full-Stack React 19 Framework
See the codeThe ejectable React 19 framework built on web standards. Build full-stack applications with native React Server Components (RSC), progressive streaming SSR, compile-time type-safe routing, dual-dimensional caching, and universal multi-platform deployment (Node.js, Bun, Deno, Cloudflare Workers, Netlify, and pure static hosts) with zero vendor lock-in.
renderToReadableStream) directly into the DOM with instant shell flushing."react-server" vs. "browser") within a single in-process pipeline via standard W3C Web Streams. Completely eliminates child_process.fork() and inter-process serialization latency, delivering an 82% memory reduction (~22 MB flat idle footprint) and enabling native execution on edge isolates.<Link href="..."> and useRouter().push(...), while strictly typing route parameters with PageProps<T>, LayoutProps<T>, RouteParams<T>, and ErrorProps<T>.src/ map directly to URLs with nested layout.tsx files that wrap child pages without remounting during navigation, preserving client state, form inputs, audio playback, and scroll positions. Layouts feature independent data and cache lifecycles (layout_functions.ts).revalidate = false), strict seconds-based Incremental Static Regeneration (revalidate = N), and dynamic on-demand Server-Side Rendering (revalidate = 0).<DinouCacheSlot> to embed high-frequency reactive sections with micro-revalidation intervals (e.g., 5s) inside long-lived static shells, refreshable independently from the client (refreshSlot) or purged globally by tag (revalidateTag).<Suspense> holes concurrently stream over a single HTTP connection."use server" can be called directly from Client Components ("use client") wrapped inside <Suspense> boundaries. They can return rendered React components over Flight RPC, eliminating synthetic REST/GraphQL API boilerplate.bun build --compile), Deno (native single-file binaries deno compile), Cloudflare Workers (native workerd + Cloudflare KV for ISR), Netlify, or pure static exports.onRequest): Intercept requests and high-frequency webhooks (Stripe, Clerk, GitHub) at the HTTP layer in dinou.config.mjs returning native Web Response objects in <2ms, completely bypassing React component rendering.@swc/core) and WebSockets that patches updates into running applications while preserving active form inputs, counters, and component state across edits.npx dinou eject (or npm run eject) at any time to extract Dinou's complete compiler, server runtime, and React 19 flight pipeline directly into your project. Transparent, web standards-based, and free of proprietary black-box macros for seamless AI pair programming.public/)
Start building modern, full-stack React 19 applications in seconds. Choose between the automated CLI generator or manual configuration for full architectural control.
Before getting started, ensure your local environment meets the minimum requirements. Dinou is engineered to leverage modern JavaScript standards with zero transpile overhead:
module.register) for zero-fork server execution and Web Standard APIs (Request, Response, ReadableStream, Headers) natively supported in modern Node.js versions.npm run build:bun:compile.npm run build:deno:compile.npm run build:cloudflare and preview locally with wrangler dev.The fastest way to scaffold a ready-to-code Dinou application is using the official CLI starter. It sets up TypeScript, Tailwind CSS, sample Server Components, and the recommended directory layout:
# 1. Initialize a new Dinou application
npx create-dinou@latest my-app
# or
npm create dinou@latest my-app
# 2. Enter project directory
cd my-app
# 3. Launch development server (default: esbuild)
npm run dev
Open http://localhost:3000 in your browser to view your live application.
Prefer to wire up everything manually? Follow these step-by-step instructions to configure Dinou from scratch with complete transparency:
Initialize your project folder and install React 19, React DOM, and the dinou framework package:
# Create and enter directory
mkdir my-dinou-app
cd my-dinou-app
# Initialize package.json
npm init -y
# Install React 19 and Dinou
npm install react react-dom dinou
# Install TypeScript definitions for IDE intellisense
npm install -D @types/react @types/react-dom
Create the src directory with a root page (src/page.tsx) and the recommended root document layout (src/layout.tsx):
// src/page.tsx - React Server Component (RSC)
export default function Page() {
return (
<main className="p-8 max-w-4xl mx-auto space-y-4">
<h1 className="text-3xl font-bold tracking-tight">
Hello from Dinou!
</h1>
<p className="text-muted-foreground">
Rendered natively on the server with React 19 and zero client JavaScript overhead.
</p>
</main>
);
}
// src/layout.tsx - Root document layout
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Dinou App</title>
</head>
<body>{children}</body>
</html>
);
}
The root layout.tsx defines the shared <html> and <body> document shell, metadata, and global providers so you don't have to repeat them on each page.
my-dinou-app/
├── public/ # Static root assets (served directly at /)
│ ├── favicon.ico
│ ├── robots.txt
│ └── site.webmanifest
├── src/
│ ├── page.tsx # Root page component (RSC)
│ ├── layout.tsx # Root document layout (<html/head/body>)
│ └── globals.css # Styles / Tailwind CSS
├── dinou.config.ts # Framework configuration (optional: ppr, plugins)
├── dinou-env.d.ts # Ambient framework typings
├── tsconfig.json # TypeScript configuration
├── package.json
└── .gitignore
Dinou does not force you to use TypeScript—you are completely free to write your application in standard JavaScript (.js / .jsx). However, even for pure JavaScript projects, defining a tsconfig.json in your project root is strongly recommended:
compilerOptions.paths from tsconfig.json to resolve clean path aliases like @/* into src/*."allowJs": true, code editors (VS Code, Cursor) use tsconfig.json to provide full autocomplete, auto-imports, and path resolution across all .js and .jsx files.{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
},
"allowJs": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": [
"src",
"dinou-env.d.ts"
]
}
Create dinou-env.d.ts in your root directory:
/// <reference types="dinou/env" />
The dinou/env reference automatically declares ambient typings for static assets (SVGs, images, web fonts) and CSS modules without manual type boilerplate.
Add the standard Dinou scripts to your package.json to control development, multi-runtime production builds (Node, Cloudflare, Deno, Bun), binary compilation, static exports, and ejecting:
{
"scripts": {
"dev": "dinou dev",
"build": "dinou build",
"start": "dinou start",
"dev:esbuild": "dinou dev:esbuild",
"dev:rollup": "dinou dev:rollup",
"dev:webpack": "dinou dev:webpack",
"build:node": "dinou build:node",
"start:node": "dinou start:node",
"build:cloudflare": "dinou build:cloudflare",
"build:deno": "dinou build:deno",
"build:deno:compile": "dinou build:deno:compile",
"start:deno": "dinou start:deno",
"build:bun": "dinou build:bun",
"build:bun:compile": "dinou build:bun:compile",
"start:bun": "dinou start:bun",
"export-static": "dinou export-static",
"eject": "dinou eject"
}
}
Note: For the complete catalog of targeted platform cross-compilation scripts (such as Windows, macOS, Linux ARM/x64), see the Complete CLI Scripts Catalog.
Create a .gitignore file to ignore dependencies, environment variables, build outputs, and Dinou caches:
node_modules
.dinou
out
dist
.wrangler
.env
.dinou/ holds internal dev artifacts and runtime builds.out/ contains pages generated by export-static.dist/ stores compiled single-executable binaries (Deno / Bun)..wrangler/ stores local state when testing Cloudflare Workers.Run the development server with the fast esbuild bundler (default), or choose Rollup or Webpack:
# Start with default ultra-fast esbuild
npm run dev
# Or start with Rollup or Webpack
npm run dev:rollup
npm run dev:webpack
Dinou provides both bundler freedom (esbuild, Rollup, Webpack) and universal runtime deployment (Node.js, Cloudflare Workers, Deno, Bun, or Static SSG) from a single codebase in src/ with zero code changes.
By default, Dinou uses esbuild for sub-second cold starts and in-memory compilation. When enterprise plugins or custom loaders are required, you can switch seamlessly to Rollup or Webpack with 100% feature parity:
# Start development with your preferred bundler
npm run dev # Default: esbuild
npm run dev:rollup # Dedicated Rollup in-memory engine
npm run dev:webpack # Dedicated Webpack engine
For advanced compiler tuning, custom plugins, and SWC options, visit the Bundlers Guide.
Compile your application for any execution environment without altering component logic:
npm run build:node && npm run start:node
npm run build:cloudflare && npx wrangler dev
npm run build:deno:compile # or build:bun:compile
Outputs standalone single-file executables directly to dist/.npm run export-static
Pre-rendered HTML and RSC Flight payloads output directly to out/.For complete deployment guides, Docker setups, and cloud hosting instructions, see the Deployment Guide.
Master Dinou's file-system router: nested layouts with independent Horizontal ISR, parallel slots with error isolation, route parameters, and layout control flags.
[!TIP] Looking for Type-Safe Routing? Dinou v7 introduces zero-runtime type safety with auto-generated route definitions, IDE autocomplete, and typed
PageProps&<Link>.
Every public route in Dinou maps directly to a folder inside src/ containing an entrypoint file (page.tsx, page.jsx, page.ts, or page.js). By default, pages are React Server Components (RSC) rendered natively on the server with zero client bundle overhead.
src/page.tsx → /src/blog/page.tsx → /blogsrc/blog/[slug]/page.tsx → /blog/hello-worldsrc/dashboard/settings/page.tsx → /dashboard/settingsYou never write central routing tables or configure URL maps manually. The folder structure inside src/ is your single source of truth.
When a request arrives (for example, /blog/hello-world), Dinou matches it entirely in memory:
src/blog/[slug]/page.tsx in memory with zero subprocess overhead or disk I/O latency.{ slug: "hello-world" }).src/layout.tsx → src/blog/layout.tsx).You can use .tsx, .ts, .jsx, or .js interchangeably across all pages and layouts.
Dinou provides expressive pattern matching for static, dynamic, optional, and catch-all URL segments:
| Pattern | Folder Structure | URL Match Example | Parsed params |
|---|---|---|---|
| Static | src/about/page.tsx | /about | {} |
| Dynamic | src/blog/[slug]/page.tsx | /blog/hello | { slug: "hello" } |
| Optional Single | src/shop/[[category]]/page.tsx | /shop/shop/shoes | { category: undefined }{ category: "shoes" } |
| Pattern | Folder Structure | URL Match Example | Parsed params |
|---|---|---|---|
| Catch-all (Required) | src/docs/[...slug]/page.tsx | /docs/routing/params | { slug: ["routing", "params"] } |
| Optional Catch-all | src/docs/[[...slug]]/page.tsx | /docs/docs/a/b | { slug: [] }{ slug: ["a", "b"] } |
Catch-all segments ([...slug]) consume all remaining path segments into a string array. Because they consume the remainder of the URL, Catch-all segments must always be the terminal (last) segment of a path definition.
Dinou enforces a strict No-Gap Rule when resolving nested optional segments such as /inventory/[[warehouse]]/[[aisle]]:
You can omit optional segments only if they are omitted from the tail end of the URL:
/inventory/main/a1 ✅ (both provided: { warehouse: "main", aisle: "a1" })/inventory/main ✅ (aisle omitted: { warehouse: "main", aisle: undefined })/inventory ✅ (both omitted: { warehouse: undefined, aisle: undefined })You cannot omit an earlier optional parameter while setting a later one:
/inventory//a1 ❌ (empty gap / skipped warehouse while setting aisle: ambiguous URL structure rejected)Dinou provides a clean, unified data contract. Route parameters are extracted from the URL and injected directly into components and server functions.
In Dinou v7, parameters are strictly typed using PageProps, LayoutProps, ErrorProps, and RouteParams:
page.tsx components: Page({ params }: PageProps<"/blog/[slug]">)layout.tsx components: Layout({ params, children }: LayoutProps<"/blog/[slug]">)error.tsx pages: Error({ error, params }: ErrorProps<"/blog/[slug]">)not_found.tsx 404 pages: NotFound({ params }: PageProps<"/blog/[slug]">)@slot/page.tsx): SlotPage({ params }: PageProps<"/dashboard/[teamId]">)getProps(params: RouteParams<"/blog/[slug]">) in page_functions.ts// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
interface BlogPostProps extends PageProps<"/blog/[slug]"> {
post?: { title: string; content: string };
}
export default function BlogPost({ params, post }: BlogPostProps) {
// ✅ params.slug is strongly typed as string automatically
return (
<article className="p-6">
<h1 className="text-2xl font-bold">{post?.title ?? params.slug}</h1>
<p className="mt-2 text-muted-foreground">{post?.content}</p>
</article>
);
}
[!IMPORTANT] Strict Signature Rule: In
page_functions.tsandlayout_functions.ts, Dinou passes the parameters dictionary directly as the first argument: usegetProps(params)andvalidateParams(params)(do not writegetProps({ params })). However, in React components (page.tsx,layout.tsx,error.tsx,not_found.tsx),paramsis passed as a component prop:export default function Page({ params }).
Exporting a validateParams(params) function from page_functions.ts or layout_functions.ts lets you enforce semantic validation rules before data fetching or rendering begins. Returning false (or a Promise that resolves to false) immediately aborts the request and issues a clean 404 response:
// src/product/[id]/page_functions.ts
import type { RouteParams } from "dinou";
export function validateParams(params: RouteParams<"/product/[id]">) {
// Ensure the product ID is strictly numeric; otherwise render 404
return /^\d+$/.test(params.id);
}
export async function getProps(params: RouteParams<"/product/[id]">) {
// Only called if validateParams returned true
const product = await db.products.findById(params.id);
return { product };
}
Guaranteed behavior across all rendering modes:
getProps, preventing wasteful database queries on invalid URLs.dinou export.Query parameters (e.g. ?search=react&sort=asc) are deliberately isolated from static route matching to preserve edge cache stability. In Dinou, useSearchParams() is a universal hook that works seamlessly in both Server Components and Client Components:
import { useSearchParams } from "dinou";
// Works identically in Server Components & "use client"!
export default function SearchPage() {
const searchParams = useSearchParams();
const q = searchParams.get("search") ?? "";
return <div>Search: {q}</div>;
}
Outside React components (where hooks cannot run), access the active query via getContext().req.query:
import { getContext } from "dinou";
// src/search/page_functions.ts
// getContext() is available in getProps, validateParams, etc.
export async function getProps() {
const { req } = getContext();
const search = req.query?.search;
return { search };
}
"use server"; // Declared at the top of the file
import { getContext } from "dinou";
// src/actions/items.ts
export async function filterItems() {
const { req } = getContext();
const search = req.query?.search;
return db.items.find({ q: search });
}
Dinou provides special files that cascade down the directory hierarchy. They allow you to compose nested layouts, manage independent ISR lifecycles, isolate errors, and break out of hierarchies using control flags.
A layout.tsx wraps all child pages and sub-layouts in its directory. On client navigation between sibling pages, parent layouts preserve their React state, input fields, scroll position, and DOM tree without re-mounting.
A layout component receives children, params, any parallel slots (e.g. sidebar), and props returned by layout_functions.ts:
// src/dashboard/[teamId]/layout.tsx
import type { ReactNode } from "react";
import type { LayoutProps } from "dinou";
interface DashboardLayoutProps extends LayoutProps<"/dashboard/[teamId]"> {
sidebar?: ReactNode; // Parallel slot (@sidebar)
}
export default function DashboardLayout({
children,
params,
sidebar,
}: DashboardLayoutProps) {
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r p-4">{sidebar}</aside>
<main className="flex-1 p-6">
<h1 className="text-xl font-bold">Team: {params.teamId}</h1>
{children}
</main>
</div>
);
}
In traditional SSR frameworks, layout shells and child pages are tightly coupled. Navigating to a dynamic child route often forces the entire layout to re-evaluate, resetting video playback, collapsing open menus, or wiping sidebar search state.
Dinou v7 introduces Horizontal Layered Segmentation. Layouts and pages travel on completely separate execution, streaming, and caching pathways:
layout_functions.ts<Page />) and streams into the layout's page slot.
page_functions.tsBecause layouts and pages are horizontally decoupled, they can declare completely independent caching lifetimes (TTL / ISR). A layout shell can cache for hours while its child pages revalidate every few seconds:
// src/dashboard/[teamId]/layout_functions.ts - Independent Layout ISR
import type { RouteParams } from "dinou";
// 1. Data Fetching for the Layout Shell
export async function getProps(params: RouteParams<"/dashboard/[teamId]">) {
const team = await fetchTeamData(params.teamId);
return { team };
}
// 2. Independent Cache Lifetime (1 hour)
// Child pages inside this dashboard can update every 5 seconds without invalidating this shell!
export const revalidate = 3600;
// 3. Granular Invalidation Tags (constant or function getCacheTags)
// Calling revalidateTag("team-shell") purges only the layout without wiping child page caches.
export const getCacheTags = ["team-shell"];
Props returned by getProps in layout_functions.ts are automatically injected into layout.tsx as top-level props alongside children, params, and any parallel slots.
Dinou allows you to break out of the nested layout hierarchy using empty flag files (files without extensions) placed in the route directory:
| Flag File | Applies To | Architectural Effect |
|---|---|---|
reset_layout | layout.tsx | Resets the layout tree. This layout becomes the new root layout, completely ignoring all parent layouts (including src/layout.tsx). Perfect for marketing landing pages or auth portals. |
no_layout | page.tsx, not_found.tsx, error.tsx | Bypasses all layouts entirely across the route. The page, custom 404, and error boundary render raw without any parent layout wrappers (ideal for embeddable widgets, checkout modals, or pure HTML canvas views). |
no_layout_error | error.tsx | Granular override: prevents parent layouts from wrapping the error boundary UI only, while preserving layouts for the regular page. |
no_layout_not_found | not_found.tsx | Granular override: prevents parent layouts from wrapping the custom 404 page only, while preserving layouts for the regular page. |
src/landing/layout.tsxsrc/landing/reset_layout (empty file)src/landing/page.tsxsrc/layout.tsx header and navigation bar.Create an error.tsx file in any route directory to catch and isolate runtime errors locally. If an exception occurs in a child component, Dinou bubbles up to the closest error.tsx, rendering it while keeping all parent layouts interactive:
// src/dashboard/[teamId]/error.tsx
import type { ErrorProps } from "dinou";
// Native: ErrorProps infers params and types the captured error
export default function DashboardError({
error,
params,
}: ErrorProps<"/dashboard/[teamId]">) {
return (
<div className="p-6 rounded-xl border border-rose-500/20 bg-rose-50/20">
<h3 className="text-rose-600 font-bold">Failed to load team {params.teamId}</h3>
<p className="text-xs text-muted-foreground mt-1">{error.message}</p>
{error.stack && (
<pre className="mt-3 p-3 bg-slate-900 text-slate-100 rounded text-xs overflow-auto">
{error.stack}
</pre>
)}
</div>
);
}
[!NOTE] Security Note: For production deployments,
error.stackis automatically stripped by Dinou to prevent internal file paths and system details from leaking. In development mode, the full cleaned stack trace is provided.
Define custom 404 views by placing not_found.tsx in any route directory. When an unmatched URL segment is requested, Dinou bubbles up to render the closest not_found.tsx template inside its enclosing layout.
// src/blog/[slug]/not_found.tsx (Scoped to /blog/[slug])
import { Link, type PageProps } from "dinou";
// Strongly types params.slug as string
export default function NotFound({ params }: PageProps<"/blog/[slug]">) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Article Not Found</h2>
<p className="mt-2 text-muted-foreground">
The article "{params.slug}" could not be located.
</p>
<Link
href="/"
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium text-sm"
>
Back to Home
</Link>
</div>
);
}
// src/not_found.tsx (Global Root Fallback)
import { Link, type PageProps } from "dinou";
// PageProps<"/"> strictly ensures no dynamic params are accidentally accessed
export default function GlobalNotFound({ params }: PageProps<"/">) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Page Not Found</h2>
<p className="mt-2 text-muted-foreground">
The requested page does not exist on this server.
</p>
<Link
href="/"
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium text-sm"
>
Back to Home
</Link>
</div>
);
}
Yes. If you define a page_functions.{ts,js} file in the same route directory as not_found.tsx, Dinou executes its getProps(params) server lifecycle and merges the returned data directly into the component's props alongside params:
// 1. Sibling server contract: src/blog/[slug]/page_functions.ts
export async function getProps(params: { slug: string }) {
const suggestions = await db.posts.findMany({ take: 3 });
return { suggestions };
}
// 2. 404 Page: src/blog/[slug]/not_found.tsx
import { Link, type PageProps } from "dinou";
interface NotFoundProps extends PageProps<"/blog/[slug]"> {
suggestions: { id: string; title: string; slug: string }[];
}
export default function NotFound({ params, suggestions }: NotFoundProps) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Article Not Found</h2>
<p className="mt-2 text-muted-foreground">
Could not find "{params.slug}". Here are some recommended articles:
</p>
{suggestions && (
<ul className="mt-4 space-y-1">
{suggestions.map((item) => (
<li key={item.id}>
<Link
href={`/blog/${item.slug}`}
className="text-primary hover:underline text-sm font-medium"
>
→ {item.title}
</Link>
</li>
))}
</ul>
)}
</div>
);
}
Difference with
error.tsx: Whilenot_found.tsxcan safely executegetProps,error.tsxpages never invoke server contracts (they receive strictlyErrorProps) to prevent compounding unhandled server exceptions.
Dinou provides dedicated directory conventions to organize enterprise codebases cleanly and compose parallel UI slots without impacting URL ergonomics:
Folders wrapped in parentheses (e.g. (auth), (marketing)) are omitted from the public URL path. They serve two fundamental architectural purposes in Dinou:
In conventional file-system routers, a single global layout forces all pages to share the same top navigation bar and footer. With Dinou Route Groups, you can create independent root layouts for marketing pages versus authentication screens:
src/
├── (marketing)/ # Public layout branch
│ ├── layout.tsx # Navbar, Footer, Hero
│ ├── page.tsx # → /
│ ├── about/page.tsx # → /about
│ └── pricing/page.tsx # → /pricing
└── (auth)/ # Minimalist auth branch
├── layout.tsx # Centered card, no Navbar
├── login/page.tsx # → /login
└── register/page.tsx # → /register
How Dinou Resolves Layouts:
src/(marketing)/layout.tsx and src/(auth)/layout.tsx live in distinct disk directories, so there is zero file collision./pricing: Dinou crawls into (marketing), matches pricing/page.tsx, and wraps it exclusively with the marketing layout./login: Dinou crawls into (auth), matches login/page.tsx, and wraps it exclusively with the auth layout.src/layout.tsx, both groups nest inside it (e.g. for HTML <head> or global fonts). If omitted, each group acts as a completely separate root layout.Because parenthesized folder names are stripped from the public URL, different route groups cannot define identical URL paths. For example, creating both src/(marketing)/about/page.tsx and src/(auth)/about/page.tsx would cause a build/resolution collision because both map to /about.
In Dinou, you do not need to prefix folders like components/, hooks/, or utils/ with an underscore. Any directory that simply does not contain a page.tsx (or page.jsx) is already completely ignored by the router and will never register a route.
The sole and specific purpose of prefixing a directory with an underscore (_folderName) is to escape the in-process route scanner when dealing with legacy code, drafts, or archived folders that actually contain page.tsx / page.jsx files inside them:
src/blog/
├── page.tsx → /blog (Active public route)
├── components/ (No page.tsx → already safe without "_")
│ └── post-card.tsx
└── _legacy_v1/ (Contains page.tsx, but "_" forces scanner to skip it!)
├── page.tsx (SKIPPED — NOT a route)
└── [slug]/page.tsx (SKIPPED — NOT a route)
Whenever Dinou's scanner encounters a folder starting with _, it completely bypasses that directory tree, ensuring that stale, archived, or legacy page files are never compiled into the production router.
Parallel routes allow you to render multiple independent pages simultaneously inside the same parent layout (e.g., @sidebar, @analytics, @modal):
Folder Structure:
src/dashboard/@sidebar/page.tsxsrc/dashboard/@analytics/page.tsxsrc/dashboard/page.tsx (main children)src/dashboard/layout.tsxLayout Prop Injection:
Slots are passed as named props directly alongside children:
import type { ReactNode } from "react";
import type { LayoutProps } from "dinou";
interface DashboardLayoutProps extends LayoutProps<"/dashboard"> {
sidebar?: ReactNode;
analytics?: ReactNode;
}
export default function Layout({
children,
sidebar,
analytics,
}: DashboardLayoutProps) {
return (
<div className="flex">
<aside>{sidebar}</aside>
<main>{children}</main>
<section>{analytics}</section>
</div>
);
}
Dinou isolates errors within parallel slots to prevent a single widget failure from crashing the entire page:
error.tsx: If the slot fails, only that specific slot renders its local error UI. The rest of the page and sibling slots remain completely operational.error.tsx: If an unhandled exception occurs in a slot with no error.tsx, Dinou safely falls back to rendering null for that slot without taking down the page.children page, and parent layouts remain fully mounted and interactive even if an individual slot fails to render.Master Dinou's client-side SPA navigation: smooth soft transitions, automatic hover prefetching, the Client Router Cache lifecycle, cache busting with the fresh prop, imperative useRouter controls, and Directory-First relative routing.
[!TIP] Looking for Type-Safe Navigation? Dinou generates automatic route types so
<Link href="...">androuter.push("...")provide zero-runtime autocomplete and compile-time error checking.
In Dinou, navigating between routes does not trigger a full browser refresh (window.location.reload). Instead, Dinou operates as a high-performance single-page application (SPA):
layout.tsx) never unmount, maintaining video playback, active form inputs, scroll positions, and local component state across page changes.startTransition, ensuring that the current UI stays fully responsive and interactive during data fetching.<Link> Component & PrefetchingThe primary way to navigate between pages is the <Link> component imported from "dinou". It intercepts click events, prevents hard reloads, and handles background prefetching:
import { Link } from "dinou";
export function NavigationBar() {
return (
<nav className="flex gap-4">
{/* ✅ Type-Safe href: autocompleted in your IDE via DinouRoute */}
<Link href="/dashboard">Dashboard</Link>
{/* ✅ Dynamic interpolation typed against registered routes */}
<Link href={`/blog/${post.slug}`}>Post</Link>
{/* Disable prefetch for heavy routes or rarely visited pages */}
<Link href="/analytics" prefetch={false}>
Analytics
</Link>
{/* ❌ TypeScript compile error on invalid/dead links when using type-safe routing */}
{/* <Link href="/broken-link">Broken</Link> */}
</nav>
);
}
[!NOTE] Native HTML
<a>Anchor Elements: In Dinou, standard HTML<a>tags also automatically trigger client-side soft navigation via global event delegation. However,<Link>is recommended because it provides compile-time type safety, background prefetching on hover, and thefreshprop.
To navigate imperatively from event handlers, timers, or form submissions, use the useRouter() hook inside Client Components ("use client"):
"use client";
import { useRouter } from "dinou";
export default function LoginButton() {
const router = useRouter();
const handleLogin = async () => {
await loginUser();
// Navigate programmatically
router.push("/dashboard");
// Or force fresh data after mutation:
// router.push("/dashboard", { fresh: true });
};
return <button onClick={handleLogin}>Log In</button>;
}
| Method | Behavior & Description |
|---|---|
router.push(href, options) | Navigates to the route and adds a new entry to the browser history stack. Supports { fresh: true }. |
router.replace(href, options) | Navigates to the route, replacing the current history entry without pushing a new history state. Supports { fresh: true }. |
router.back() | Navigates back to the previous page in history (equivalent to clicking the browser Back button). |
router.forward() | Navigates forward to the next page in the history stack. |
router.refresh() | Soft Reload: Re-fetches the current route data from the server and updates server components without losing client React state or scroll position. |
router.refreshSlot(slotId) | Re-fetches and updates an individual <DinouCacheSlot> component without re-rendering the surrounding page. |
To deliver instant page transitions without unnecessary server requests, Dinou features an in-memory Client Router Cache. Understanding how this cache operates is key to knowing when and why to use the fresh prop.
During client execution, Dinou maintains three dedicated in-memory caches in the browser tab:
pageCache: Stores prefetched and visited RSC promises keyed by pathname + search.scrollCache: Retains exact window and container scroll coordinates keyed by history state ID.popstate: Reads from pageCache and restores scroll without network requests on back/forward.<Link href="/pricing">, Dinou automatically prefetches the route payload in the background and stores the pending RSC promise directly into pageCache."/pricing". Because the payload was already prefetched into pageCache, the transition renders instantly with 0ms network latency.popstate events read from pageCache and restore the previous scroll position from scrollCache without making any network requests.While caching provides instantaneous navigation, pages with volatile data or pages visited immediately after a mutation (e.g., checkout, profile updates, or inventory counters) need fresh server state. The fresh prop provides explicit, predictable cache busting:
<Link fresh> does not prefetch potentially stale data before the user actually clicks.pageCache.delete(targetUrl), removing any prior cached RSC promise.// Ideal for live feeds, stock tickers, or carts
<Link href="/live-feed" fresh>
Live Feed
</Link>
// Navigate after a mutation to force fresh server data
const handleUpdate = async () => {
await updateProfile(data);
router.push("/profile", { fresh: true });
};
| Mechanism / Scope | Cache Behavior | Network Request | Typical Use Case |
|---|---|---|---|
Standard <Link> | Uses pageCache if prefetched or visited | 0ms if in cache; single request on first visit | General navigation, blogs, docs, marketing pages |
<Link fresh> / { fresh: true } | Purges pageCache.delete(url) | Always fetches live RSC payload from server | Post-mutation redirects, active carts, volatile feeds |
router.refresh() | Purges pageCache.delete(currentPath) | Re-fetches current route in-place | Polling, server function response handling, reload button |
| Hard Reload (F5) | Destroys all memory (all caches lost) | Full HTML document download + re-hydration | Emergency fallback; loses all client state |
In Dinou, all client-side navigations execute concurrently inside React 19's startTransition. This ensures that the current view remains fully interactive and visible while the target page's Server Components stream in from the server.
To inform users that a transition is occurring behind the scenes, Dinou provides the useNavigationLoading() hook:
true while a route payload is actively streaming or preparing in the background, and resets to false the moment React 19 commits the new page tree.false during server-side rendering (SSR), preventing hydration mismatches and flickering when the document first mounts.Mount a client loading indicator in your root layout to provide visual feedback (similar to GitHub or YouTube) whenever a route transition is pending:
"use client";
import { useNavigationLoading } from "dinou";
export function NavigationProgressBar() {
const isNavigating = useNavigationLoading();
if (!isNavigating) return null;
return (
<div
role="progressbar"
aria-label="Page loading"
className="fixed top-0 left-0 right-0 z-50 h-1 bg-blue-500/20 overflow-hidden pointer-events-none"
>
<div className="h-full bg-blue-600 dark:bg-blue-400 animate-pulse w-full transition-all duration-300" />
</div>
);
}
[!TIP] Because prefetched links resolve instantly in 0ms directly from
pageCache, this progress bar only surfaces on fresh fetches (such as navigation withfresh: trueor un-prefetched dynamic routes), keeping transitions ultra snappy.
Dinou resolves relative navigation paths (such as about, ./about, or ../dashboard) using a Directory-First Convention. This provides intuitive navigation that accurately mirrors your file system.
In a standard web browser, a URL like /blog/hello (without a trailing slash) treats hello as a file name, so a link to about resolves to /blog/about.
In Dinou, every route is treated as a directory because physically, each page is a folder containing a page.tsx component (e.g. src/blog/hello/page.tsx). Resolving relatively from a page navigates relative to that route's folder:
| Current Path | Relative Target | Resolved Path |
|---|---|---|
/blog/hello | about | /blog/hello/about |
/blog/hello | ./about | /blog/hello/about |
/blog/hello | ../about | /blog/about |
This rule applies uniformly across all navigation methods, including the <Link> component and programmatic calls like router.push().
Dinou v7 introduces a built-in, compile-time Type-Safe Routing engine with zero runtime overhead. During development and build time, Dinou automatically inspects your src/ directory and generates ambient TypeScript definitions in .dinou/types/routes.d.ts.
Unlike traditional routing setups that require external schema validators or proprietary bundler plugins, Dinou generates strict TypeScript definitions directly from your file system.
The system operates across two complementary architectural planes:
<Link href="...">, router.push("..."), or passing routes to reusable UI components with DinouRoute, TypeScript autocompletes valid routes, verifies dynamic template strings, and raises compile-time errors on dead links or typos.PageProps<T>, LayoutProps<T>, ErrorProps<T>, and RouteParams<T>.All route type validation occurs entirely within the TypeScript compiler. The emitted .dinou/types/routes.d.ts file contains ambient type declarations and is never bundled into your client JavaScript or server execution bundle:
Import <Link> or useRouter from "dinou". Your IDE will immediately suggest all valid static routes, parameterized dynamic routes, relative paths, queries, and hashes:
import { Link, useRouter } from "dinou";
export default function Navigation({ post }: { post: { slug: string } }) {
const router = useRouter();
return (
<nav className="flex gap-4">
{/* ✅ Instant autocompletion in your IDE */}
<Link href="/docs/routing">Routing Guide</Link>
{/* ✅ Dynamic routes with typed template strings */}
<Link href={`/blog/${post.slug}`}>View Post</Link>
{/* ✅ Seamless support for queries, hashes, relative paths, and external URLs */}
<Link href="/demo?tab=settings#section-1">Settings</Link>
<Link href="../contact">Relative</Link>
<Link href="https://github.com/roggc/dinou">GitHub</Link>
{/* ❌ TypeScript compile-time error when pointing to a non-existent route */}
{/* <Link href="/unknown-route" /> */}
<button onClick={() => router.push("/docs/api-reference")}>
View API Reference
</button>
</nav>
);
}
Type your components and data fetching functions by passing the route template string as a generic:
import type { PageProps, LayoutProps, RouteParams } from "dinou";
// 1. src/blog/[slug]/page.tsx
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed as string
return <h1>Post: {params.slug}</h1>;
// ❌ TypeScript error: Property 'id' does not exist on type '{ slug: string }'
// return <h1>{params.id}</h1>;
}
// 2. src/blog/[slug]/layout.tsx
export function BlogLayout({ children, params }: LayoutProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed; children is typed as ReactNode
return (
<section>
<header>Category: {params.slug}</header>
<main>{children}</main>
</section>
);
}
// 3. src/blog/[slug]/page_functions.ts
export async function getProps(params: RouteParams<"/blog/[slug]">) {
// ✅ Directly types the raw params object without params: any
return {
postTitle: `Post: ${params.slug}`,
};
}
| Exported Type | Primary Purpose | Canonical Example |
|---|---|---|
PageProps<T> | React Page components (page.tsx) | Page({ params }: PageProps<"/blog/[slug]">) |
LayoutProps<T> | React Layout components (layout.tsx) | Layout({ children, params }: LayoutProps<"/blog/[slug]">) |
ErrorProps<T> | React Error Page components (error.tsx) | Error({ error, params }: ErrorProps<"/blog/[slug]">) |
RouteParams<T> | Pure functions, data loaders & services (getProps, API helpers) | getProps(params: RouteParams<"/blog/[slug]">) |
DinouRoute | URL props, <Link>, router.push() & custom navigation components | interface NavProps { href: DinouRoute } |
Dinou deterministically transforms your file-system folder naming conventions into strict TypeScript definitions:
| Filesystem Convention | Inferred Parameter Type | Route Example | Type of params |
|---|---|---|---|
| Static Route | No parameters | src/about/page.tsx | Record<string, never> |
Single Dynamic [id] | string | src/blog/[id]/page.tsx | { id: string } |
Catch-All [...slug] | string[] | src/docs/[...slug]/page.tsx | { slug: string[] } |
Optional Dynamic [[lang]] | string | undefined | src/[[lang]]/page.tsx | { lang?: string } |
Optional Catch-All [[...opt]] | string[] | undefined | src/shop/[[...opt]]/page.tsx | { opt?: string[] } |
| Multiple Dynamic Segments | Combined by segment | src/users/[uId]/posts/[pId]/page.tsx | { uId: string; pId: string } |
| Nested Optionals | Positional dependent | src/inv/[[wh]]/[[aisle]]/page.tsx | { wh?: string; aisle?: string } |
[!WARNING] Be cautious with trailing slashes in dynamic links!
In TypeScript, template literal types evaluate
${string}as matching any string, including the empty string""(because"" extends stringevaluates totrue).The scenario: Consider a nested route such as
src/posts/[id]/[slug]/page.tsx, whose generated template literal type is`/posts/${string | number}/${string | number}`.If a developer accidentally leaves off the second parameter but appends a trailing slash:
<Link href={`/posts/${id}/`}>View</Link>TypeScript matches the first parameter with
id, the slash with literal/, and the second parameter with empty string"". However, at runtime Dinou normalizes trailing slashes, requesting/posts/123(which results in a 404 Not Found because nopage.tsxexists at the parent segment).Best Practice: Always avoid trailing slashes when building dynamic URLs. Written without the trailing slash (
`/posts/${id}`), TypeScript accurately flags the missing parameter at compile-time.
When typing a component, type PageProps<""> and press Ctrl + Space (or Cmd + Space on macOS). Your editor will list every registered route pattern in the project:
import type { PageProps } from "dinou";
// Trigger Ctrl + Space inside "" to pick from project routes
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
return <h1>{params.slug}</h1>;
}
Error boundaries in Dinou v7 strongly type both the captured error and the current route's parameters:
"use client";
import { useRouter, type ErrorProps } from "dinou";
export default function BlogPostError({ params, error }: ErrorProps<"/blog/[slug]">) {
const router = useRouter();
return (
<div className="error-card p-6 border rounded-xl bg-destructive/10">
<h2>Failed to load post: {params.slug}</h2>
<p className="text-sm text-muted-foreground">{error.message}</p>
{/* Soft reload via Dinou client router */}
<button onClick={() => router.refresh()}>Try Again</button>
</div>
);
}
When creating design-system components (e.g., custom buttons, sidebar items, dropdown menus), use the exported DinouRoute union type to pass through autocomplete and validation:
import type { DinouRoute } from "dinou";
import { Link } from "dinou";
interface NavButtonProps {
href: DinouRoute; // 👈 Inherits full autocompletion and route validation
label: string;
}
export function NavButton({ href, label }: NavButtonProps) {
return (
<Link href={href} className="px-3 py-1.5 rounded-lg bg-blue-600 text-white font-medium">
{label}
</Link>
);
}
// In your application code:
const menu: { label: string; href: DinouRoute }[] = [
{ label: "Overview", href: "/docs" }, // ✅ Autocompleted
{ label: "Routing", href: "/docs/routing" }, // ✅ Autocompleted
// { label: "Broken", href: "/non-existent-page" }, // ❌ TypeScript compile error!
];
Dinou types template literal strings against dynamic route definitions, catching broken template strings while accepting valid parameters:
import { Link, useRouter } from "dinou";
interface Product {
id: string;
name: string;
}
export function ProductCard({ product }: { product: Product }) {
const router = useRouter();
return (
<div className="card p-4 border rounded-xl">
{/* String interpolation verified against `/products/${string | number}` */}
<Link href={`/products/${product.id}`}>{product.name}</Link>
<button onClick={() => router.push(`/products/${product.id}`)}>
View Details
</button>
</div>
);
}
Dinou coordinates compilation, route scanning, and TypeScript declarations through a zero-dependency architecture:
src/ Directory: Synchronous read and structural crawl of all page and layout files.generate-route-types): Low-overhead in-memory scanner completes in under 2ms..dinou/types/routes.d.ts): Emits ambient module declarations extending the "dinou" package interface with exact union types (DinouRoute) and parameter maps (DinouRouteParams).tsconfig.json without requiring editor restarts or language server reboots.How Dinou keeps route typings strictly synchronized across local development and deployment pipelines:
npm run dev, Dinou arms a low-overhead filesystem watcher on src/. Whenever a page.(tsx|jsx|ts|js) file is added, renamed, or deleted, .dinou/types/routes.d.ts regenerates in under 2ms.npm run build runs route type generation as step 0 before bundling. If code references a non-existent route, TypeScript type checking fails the build before deploying.npx tsc --noEmit in CI pipelines to validate link integrity across the entire project deterministically.export default function Page({ params }: any) or generic PageProps during progressive migrations..dinou/types/routes.d.ts has not yet been generated (e.g., fresh clone prior to npm run dev or npm run build), types gracefully fallback to permissive strings (string & {}), ensuring clean git checkout and initial install steps without blocking developer workflows.Dinou is built natively on the React 19 Server Components (RSC) specification. Rather than treating your entire application as a single monolithic client-side bundle, Dinou partitions components into two distinct execution environments:
"use client". Also pre-rendered to HTML on the server (SSR) for instant first paint, and then hydrated with interactive JavaScript in the browser to enable local state (useState, useActionState), event listeners (onClick), and browser APIs.Dinou's dual-component model allows you to seamlessly mix server-rendered backbones with interactive client islands:
| Feature | Server Components (RSC) | Client Components |
|---|---|---|
| Default in Dinou? | Yes (all components inside src/) | No (requires explicit "use client") |
| Execution Environment | Server only (Node.js, Bun, Deno, Cloudflare Workers) | Server (pre-rendering SSR) + Browser (hydration) |
| Client JS Bundle Impact | 0 KB (Zero client JavaScript overhead) | Ships component JS code to the browser |
| Backend & Secret Access | Direct access to DB, ORM, filesystem, private env vars | Cannot access backend resources or private secrets |
| Interactivity & State | Pure static output; no state or DOM event listeners | Full React state, hooks, effects, and DOM events |
| Data Fetching Pattern | Direct top-level await inside component body | Props passed from RSC, React 19 use(), or Server Functions |
Think of Server Components as the backbone of your application that orchestrates data fetching, layouts, and non-interactive UI (rendering pure HTML with zero client JavaScript). Client Components act as interactive "islands" or leaves embedded inside the server tree.
The server streams HTML and React Flight JSON representations across an open HTTP connection. The browser consumes this stream without ever downloading the JavaScript source code of your Server Components.
In Dinou, every component inside src/ is a Server Component by default unless explicitly marked with "use client". They execute exclusively on the server and never ship their implementation code to the browser.
async functions. You can await database queries, ORM calls, microservices, or files directly inside the component body.<Suspense>: When slow asynchronous operations are wrapped in React <Suspense>, the layout shell and fallback UI can be delivered immediately without waiting for slow queries, while data streams progressively into the HTTP response.// src/users/page.tsx (Server Component by default)
import { getContext } from "dinou";
import { db } from "@/lib/db"; // Never bundled to the client!
export default async function UsersPage() {
// Direct backend query without an intermediary REST API
const users = await db.user.findMany({ take: 10 });
return (
<main className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold">Registered Users</h1>
<ul className="mt-4 divide-y">
{users.map((user) => (
<li key={user.id} className="py-2 flex justify-between">
<span>{user.name}</span>
<span className="text-xs font-mono text-muted-foreground">{user.email}</span>
</li>
))}
</ul>
</main>
);
}
[!NOTE] TTFB & Execution Timing Note:
- Static by Default (SSG): In Dinou, this page is pre-rendered at build time by default. The database query runs during
npm run build, serving static HTML to visitors with instant TTFB (0ms server wait).- Dynamic Routes: If the route is marked dynamic (e.g.
revalidate = 0inpage_functions.tsor reading cookies), a top-levelawaitblocks the entire HTTP response until the query finishes. To preserve instant TTFB on dynamic routes, delegate theawaitto a child component wrapped in React<Suspense>.
<Suspense>By delegating asynchronous queries to child components wrapped in <Suspense>, the outer layout and fallback UI are sent immediately:
// src/users/page.tsx - Non-blocking streaming layout
import { Suspense } from "react";
import { db } from "@/lib/db";
import { UsersSkeleton } from "./skeleton";
export default function UsersPage() {
return (
<main className="p-8 max-w-4xl mx-auto">
{/* 1. The outer shell is sent immediately (instant TTFB) */}
<h1 className="text-2xl font-bold">Registered Users</h1>
{/* 2. Displays fallback while the child query resolves */}
<Suspense fallback={<UsersSkeleton />}>
<UsersList />
</Suspense>
</main>
);
}
// Child component handles the async query independently
async function UsersList() {
const users = await db.user.findMany({ take: 10 });
return (
<ul className="mt-4 divide-y">
{users.map((u) => (
<li key={u.id} className="py-2 flex justify-between">
<span>{u.name}</span>
<span className="text-xs font-mono text-muted-foreground">{u.email}</span>
</li>
))}
</ul>
);
}
<Suspense> Behaves Across Rendering Strategies| Strategy | Compiler & Runtime Behavior | Fallback Skeleton Behavior | Trade-off |
|---|---|---|---|
| Pure SSG (Static) | The compiler automatically awaits all Suspense boundaries during npm run build and writes the resolved HTML directly into the static artifact. | Visitors never see the loading skeleton because the fallback is eliminated once the build settles. | 0ms TTFB from CDN edge, but data reflects build-time state (stale until next build/revalidation). |
| Pure Dynamic SSR | The server flushes the layout shell and <Suspense fallback={<Skeleton />}> immediately over the open HTTP connection. | The user sees the skeleton while waiting, and React streams the resolved HTML chunk when the query finishes. | 100% fresh data and non-blocking layout, but incurs continuous server compute costs, DB load, and request-time latency. |
| Partial Prerendering (PPR) | The outer layout shell and the fallback skeleton are pre-rendered at build time and served immediately with 0ms TTFB from the CDN edge. | The skeleton renders instantly from static cache, while the server concurrently executes only the dynamic query and streams live data into the hole. | Combines the instant delivery of SSG with the fresh, live data of dynamic SSR at minimal server compute cost. |
Because Server Components do not execute in the browser, they cannot use browser-specific or client-interactive features:
useState, useEffect, useReducer, useLayoutEffect.onClick, onChange, onSubmit, onKeyDown.window, document, localStorage, navigator, geolocation.useContext() or use(Context). (While Server Components can render <Context> providers for client children, they cannot read context values themselves).Client Components provide rich, real-time client-side interactivity. They enable event handlers, local component state, animations, and access to browser APIs.
Client Components have complete access to the client-side React ecosystem:
useState, useReducer, and React 19 useActionState.useEffect and useLayoutEffect.localStorage, Geolocation, IntersectionObserver, Audio).A common point of confusion is assuming Client Components execute exclusively in the browser. In Dinou, Client Components are pre-rendered on the server to HTML during initial page load to ensure instant First Contentful Paint (FCP) and optimal SEO indexing before hydrating in the browser.
[!WARNING] SSR Execution Safety Rule: Because Client Components run on the server during pre-rendering, accessing browser globals directly in the root of the component function (e.g.
const token = localStorage.getItem("token")) will throw a server runtime error.Always defer browser-only API access inside
useEffector guard it with an environment check:if (typeof window !== "undefined") { const token = localStorage.getItem("auth_token"); }
"use client" DirectiveTo declare a component as a Client Component, add the "use client" directive at the very top of the file, before any import statements or comments.
The directive is not a runtime function call—it is an instruction to the bundler (esbuild, Rollup, Webpack) that defines the network split point:
"use client"; // Must be at the very top of the file!
import { useState } from "react";
export function Counter({ initial = 0 }: { initial?: number }) {
const [count, setCount] = useState(initial);
return (
<button
onClick={() => setCount((c) => c + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium"
>
Count: {count}
</button>
);
}
Once a file declares "use client", all components, utility modules, and third-party libraries imported by that file are automatically treated as part of the client bundle. You do not need to add "use client" to child modules imported by a Client Component.
When passing props from a Server Component to a Client Component, data crosses the network boundary (serialized as React Flight JSON). Props passed across this boundary must be serializable.
string, number, boolean, null, undefined, bigint.{ id: 1, name: "Dinou" }).Date objects are serialized and reconstructed by React on the client.children or named slot props.use())."use server".onClick={() => ...}) from Server Components.window, document.body).use()React 19 supports passing an unresolved Promise as a prop from a Server Component to a Client Component. The Client Component reads the promise synchronously using the use() hook, automatically triggering Suspense fallbacks until the promise settles:
// Server Component
import { Suspense } from "react";
import { UserProfile } from "./user-profile"; // "use client"
export default function Page() {
// Start fetching without awaiting!
const userPromise = fetchUserData();
return (
<Suspense fallback={<div>Loading profile skeleton...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Client Component (user-profile.tsx)
"use client";
import { use } from "react";
export function UserProfile({ userPromise }: { userPromise: Promise<{ name: string; role: string }> }) {
// Unwraps the promise value seamlessly inside Suspense without useEffect
const user = use(userPromise);
return <h2>Hello, {user.name} ({user.role})</h2>;
}
Dinou empowers you to choose the exact architectural balance between server performance and client ergonomics. Follow these battle-tested patterns for optimal architecture:
Keep your layouts, route pages, and static wrappers as Server Components. Only push the "use client" boundary down to small, interactive "leaf" components (such as an upvote button, search box, or dropdown menu).
src/dashboard/layout.tsx → Server Component (zero client JS)
src/dashboard/page.tsx → Server Component (fetches data directly)
src/dashboard/_components/search-bar.tsx → "use client" (interactive leaf)
src/dashboard/_components/action-button.tsx → "use client" (interactive leaf)
A Client Component cannot directly import a Server Component (because bundling a Server Component into the client module would defeat the RSC boundary). However, a Client Component can receive Server Components as children or via named JSX props!
// Client Component: ModalContainer.tsx
"use client";
import { useState, type ReactNode } from "react";
export function ModalContainer({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<div className="modal-backdrop">
{/* children is rendered on the server and passed as pre-rendered JSX! */}
{children}
</div>
)}
</div>
);
}
// Server Component: page.tsx
import { ModalContainer } from "./ModalContainer";
import { HeavyServerProfile } from "./HeavyServerProfile"; // Server Component
export default function Page() {
return (
<ModalContainer>
{/* HeavyServerProfile remains 100% on the server! */}
<HeavyServerProfile />
</ModalContainer>
);
}
getProps)If you are building an interactive canvas, video editor, or complex SPA dashboard and prefer traditional React where you can use useState and onClick anywhere in the page without boundary splitting, use Dinou's Client-First Model:
// src/canvas/page_functions.ts - Runs securely on the server
import { db } from "@/lib/db";
export async function getProps(params: { id: string }) {
const project = await db.projects.findById(params.id);
return { project };
}
// src/canvas/page.tsx - Entire page is a Client Component
"use client";
import { useState } from "react";
export default function CanvasPage({ project }: { project: any }) {
// Full access to state, effects, and events everywhere in the file!
const [layers, setLayers] = useState(project.initialLayers);
return (
<div onClick={() => console.log("clicked")}>
<Toolbar onAddLayer={(l) => setLayers([...layers, l])} />
<Stage layers={layers} />
</div>
);
}
[!TIP] Advantage: Fast development speed and traditional React ergonomics, while preserving secure backend data fetching and SSR pre-rendering.
<DinouCacheSlot> (Vertical Micro-ISR)A common architectural trap in dynamic RSC applications is converting heavy server widgets into Client Components just to gain client-side memory caching (such as SWR or TanStack Query). In Dinou, <DinouCacheSlot> provides component-level server caching (Vertical Segmentation) directly in the RSC layer.
"use client" with a client cache. This bloats the client JS bundle, loses direct database access, exposes internal API endpoints, and introduces client-side network waterfalls.<DinouCacheSlot>): Keep the component on the server! <DinouCacheSlot> caches the rendered RSC Flight payload of that specific subtree on the server for a designated TTL (e.g. 300s). The rest of the page remains 100% dynamic, while the expensive subtree serves in 0ms.<DinouCacheSlot> Contain Client Components?Yes, absolutely. While <DinouCacheSlot> itself is an async Server Component that executes exclusively on the server, its children can freely render interactive Client Components. Dinou caches the Client Component reference along with its pre-computed props. The client hydrates it normally with full interactivity (events, state, animations):
// 1. Client Component: interactive-chart.tsx (Hydrates with 100% interactivity)
"use client";
import { useState } from "react";
export function InteractiveChart({ data }: { data: any }) {
const [zoom, setZoom] = useState(1);
return (
<div className="p-4 border rounded-lg bg-card">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-bold">Metrics (Zoom: {zoom}x)</span>
<button
onClick={() => setZoom((z) => z + 1)}
className="px-2 py-1 text-xs bg-primary text-primary-foreground rounded"
>
Zoom In
</button>
</div>
<canvas data-zoom={zoom} />
</div>
);
}
// 2. Server Component: page.tsx (Dynamic page with an isolated cached slot)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { UserGreeting } from "./UserGreeting"; // Reads user session/cookies
import { GreetingSkeleton, AnalyticsSkeleton } from "./skeletons";
import { InteractiveChart } from "./interactive-chart"; // Client Component
// Server Component performing the heavy query
async function HeavyAnalyticsWidget() {
const analyticsData = await db.analytics.aggregateBigData(); // 2-second query
return <InteractiveChart data={analyticsData} />;
}
export default function DashboardPage() {
return (
<main className="space-y-6 p-8">
{/* Dynamic personal component: reads cookies/session without blocking initial layout delivery (preserves fast TTFB) */}
<Suspense fallback={<GreetingSkeleton />}>
<UserGreeting />
</Suspense>
{/* Micro-ISR hole: wrapped in Suspense so cache misses stream without blocking TTFB */}
<Suspense fallback={<AnalyticsSkeleton />}>
<DinouCacheSlot id="analytics-slot" duration={300} tags={["analytics"]}>
<HeavyAnalyticsWidget />
</DinouCacheSlot>
</Suspense>
</main>
);
}
<Suspense>? While <DinouCacheSlot> works without it on cache hits, on a cache miss (cold start or expired TTL) an unwrapped slot would block the entire HTTP response during the heavy query. Wrapping it in <Suspense> guarantees instant TTFB by streaming the layout and skeleton immediately.UserGreeting? In dynamic SSR, wrapping UserGreeting in <Suspense> ensures that reading user session or auth cookies does not delay the initial response, keeping TTFB fast. In PPR, it also prevents dynamic request data from de-optimizing the build-time static shell.refreshSlot("analytics-slot") from "dinou" without reloading the page or resetting state in other components.revalidateTag("analytics").<DinouCacheSlot>What happens when you nest <DinouCacheSlot> inside a React <Suspense> boundary on a page with Partial Prerendering (PPR)? You unlock the highest performance profile possible in full-stack web engineering: zero-delay initial delivery paired with near-zero streaming delay and 99.9% database query offloading.
| Metric | PPR Alone (Without Slot) | PPR + <DinouCacheSlot> |
|---|---|---|
| Architecture | Static shell + on-demand dynamic execution | Instant shell + cached streaming hole |
| TTFB (First Byte) | 0ms from CDN edge (instant outer layout) | 0ms from CDN edge (instant outer layout) |
| Skeleton Duration | The user waits the full query duration (e.g. 1,200ms) on every single request while the server executes the query. | ~5ms! Subsequent visitors receive the cached RSC stream chunk almost immediately; the skeleton barely blinks. |
| Database Load | 10,000 visitors per minute = 10,000 database queries executed on the server. | 10,000 visitors per minute with duration={60} = 1 database query per minute (99.99% reduction). |
Wrap the slow asynchronous Server Component with <DinouCacheSlot>, and then enclose both inside React <Suspense>:
// src/dashboard/page.tsx - High-traffic operational dashboard
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { MetricsSkeleton } from "./MetricsSkeleton";
import { InteractiveMetricsChart } from "./InteractiveMetricsChart"; // "use client"
// Heavy async Server Component
async function HeavyMetricsSubtree() {
// Heavy aggregation across millions of database rows (e.g. 1.5 seconds)
const aggregatedStats = await db.telemetry.aggregateOperations();
return <InteractiveMetricsChart data={aggregatedStats} />;
}
export default function DashboardPage() {
return (
<main className="p-8 space-y-6 max-w-5xl mx-auto">
{/* 1. PPR Static Shell: Delivered instantly with 0ms TTFB from edge CDN */}
<header className="border-b pb-4">
<h1 className="text-3xl font-extrabold tracking-tight">Operations Command Center</h1>
<p className="text-sm text-muted-foreground">Real-time system telemetry and infrastructure health.</p>
</header>
{/* 2. PPR Dynamic Boundary + Cache Slot:
- Fallback skeleton sent immediately in initial layout
- Dynamic hole streams from server cache in ~5ms for subsequent users
- Database query executes only ONCE every 60 seconds */}
<Suspense fallback={<MetricsSkeleton />}>
<DinouCacheSlot id="operations-telemetry" duration={60} tags={["telemetry"]}>
<HeavyMetricsSubtree />
</DinouCacheSlot>
</Suspense>
</main>
);
}
0ms from CDN edge.revalidateTag("telemetry") or refreshSlot() to update without full-page reloads.<DinouCacheSlot>?A common question in full-stack architecture is: Does <DinouCacheSlot> only make sense for dynamic components or subtrees?
The short answer: While dynamic routes and PPR are where it delivers exponential returns, it also unlocks unique capabilities for static pages (such as asymmetric revalidation and in-place client slot refreshes). Understanding when to use it boils down to the distinction between Horizontal Segmentation (route/layout-level) and Vertical Segmentation (component-level).
<DinouCacheSlot> (Redundant or Anti-Patterns)page.tsx body inside a slot reinvents horizontal caching. Use export const revalidate = 300 in page_functions.ts instead.<DinouCacheSlot> Delivers 10x Value (Recommended Scenarios)<DinouCacheSlot> isolates the expensive widget with its own TTL:// src/routes/dashboard/page.tsx (Dynamic authenticated SSR)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { getSession } from "@/lib/auth";
import { GlobalMetricsWidget } from "./metrics";
export default async function DashboardPage() {
const session = await getSession(); // Evaluated dynamically per user
return (
<div className="p-6">
<h1>Welcome back, {session.user.name}</h1> {/* 100% Live & personal */}
{/* Global widget: cached for 10 minutes on the server across all users */}
<Suspense fallback={<div>Loading global telemetry...</div>}>
<DinouCacheSlot id="global-metrics" duration={600} tags={["metrics"]}>
<GlobalMetricsWidget />
</DinouCacheSlot>
</Suspense>
</div>
);
}
Dynamic Holes in Partial Prerendering (PPR) — The Ultimate Synergy: In PPR, the static shell ships instantly from CDN (0ms TTFB), while dynamic holes stream in via React Suspense. Without a slot, 100,000 visitors trigger 100,000 database hits. Placing <DinouCacheSlot> inside the hole means only the first request queries the DB; the other 99,999 stream the cached RSC Flight payload in ~2-5ms.
Static or ISR Pages with Asymmetric Lifecycles & Targeted Refreshes: This is where <DinouCacheSlot> proves it is not limited to dynamic pages. You can use it in static/ISR pages for two critical architectural advantages:
revalidate = 86400). But live stock pricing fluctuates every 60 seconds. A slot lets the pricing widget expire every 60s without re-rendering the whole page!refreshSlot): A Client Component can trigger an on-demand re-render of only this Server Component using refreshSlot(slotId) without refreshing the browser page or losing client state (scroll position, inputs, audio):// 1. In your Page (Server Component): Asymmetric slot inside static route
<DinouCacheSlot id={`price-${product.id}`} duration={60} tags={["pricing"]}>
<LivePriceCalculator productId={product.id} />
</DinouCacheSlot>
// 2. In an interactive Client Component: refresh ONLY that slot
"use client";
import { refreshSlot } from "dinou";
export function RefreshPriceButton({ productId }: { productId: string }) {
return (
<button onClick={() => refreshSlot(`price-${productId}`, { fresh: true })}>
Check Latest Stock & Price
</button>
);
}
| Question / Requirement | Recommended Action | Reason |
|---|---|---|
| Dynamic or PPR route with heavy queries? | Wrap with <DinouCacheSlot> inside <Suspense> | Protects database from repetitive execution and streams cached RSC Flight chunks in ~2-5ms. |
| Asymmetric expiration within a page? | Use <DinouCacheSlot> | Decouples widget TTL (e.g. 60s) from overall page TTL (e.g. 24h) without re-rendering the entire page. |
| Need surgical, targeted client refreshes? | Use <DinouCacheSlot> + refreshSlot() | Updates just that server subtree in-place without page reload or state loss. |
| Pure SSG with uniform expiration? | Do NOT use <DinouCacheSlot> | Let build-time SSG and CDN Edge caching handle the entire page with zero runtime overhead. |
"use client" at the root layout. Marking src/layout.tsx as a Client Component cascades the client boundary to every single page in your application. Root layouts should always be Server Components to protect secrets and enable streaming.import ServerComponent from "./ServerComponent" inside a file containing "use client". Use the Composition Pattern (Section 6.2) and pass it as children instead.window.innerWidth or localStorage.getItem() outside of a useEffect in a Client Component causes server pre-rendering crashes. Always wrap browser calls in useEffect or an environment check (if (typeof window !== "undefined"))."use server") for actions.useEffect on mount. In modern RSC architectures, fetching on mount via useEffect creates network waterfalls, causes layout shifts, and misses server streaming. Pass data from Server Components or use Strategy 9 (RSC Promise) / Strategy 12 (Server Function RPC).In Dinou, every route segment can independently configure its data fetching, static pre-rendering, cache lifecycles, and parameter validation without cluttering component UI code.
page_functions.ts: Placed alongside page.tsx. Configures data fetching (getProps), static paths (getStaticPaths), cache strategy (revalidate), Partial Prerendering (ppr), cache tags (getCacheTags), and parameter validation (validateParams) specifically for that page.layout_functions.ts: Placed alongside layout.tsx. Supplies data directly to the layout component (getProps) and manages segment-level cache strategy (revalidate, ppr) independently from child pages.Dinou strictly separates server-side data orchestration and cache configuration from presentation UI:
src/blog/
├── layout.tsx # Presentation shell (RSC)
├── layout_functions.ts # Layout cache strategy (revalidate = 300) & getProps
└── [slug]/
├── page.tsx # Page presentation (RSC or "use client")
└── page_functions.ts # Page data fetching (getProps), paths, and validation
RouteSegmentConfig<TParams, TProps>In Dinou, every server-side configuration file implements the exact same foundational contract: RouteSegmentConfig. This is the framework's underlying "abstract" interface defining all segment capabilities. For day-to-day application code, Dinou exports two concrete, role-based aliases: PageFunctions (for pages) and LayoutFunctions (for layouts):
// Exported directly from "dinou":
/**
* Foundational "abstract" interface governing all route segments in Dinou.
*/
export interface RouteSegmentConfig<TParams = any, TProps = any> {
// Partial Prerendering (PPR) configuration
ppr?: boolean | (() => boolean | Promise<boolean>);
// Cache & revalidation strategy (< 1 = Dynamic SSR, >= 1 = ISR, false = Static)
revalidate?: number | false | (() => number | false | Promise<number | false>);
// Cache tags for on-demand invalidation via revalidateTag(tag)
getCacheTags?: string[] | ((params?: TParams) => string[] | Promise<string[]>);
// Path generator for SSG (constant array or sync/async function)
getStaticPaths?: StaticPathItem<TParams>[] | (() => StaticPathsResult<TParams>);
// Server loader providing direct props to the component
getProps?: (params?: TParams) => TProps | Promise<TProps>;
// Parameter validator guard (returns false to halt and return 404)
validateParams?: (params?: TParams) => boolean | Promise<boolean>;
// Controls whether Incremental Static Generation (ISG) is allowed for ungenerated paths
allowISG?: boolean | (() => boolean | Promise<boolean>);
}
/**
* Concrete semantic alias to import and use in 'page_functions.ts'
*/
export type PageFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;
/**
* Concrete semantic alias to import and use in 'layout_functions.ts'
*/
export type LayoutFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;
| Where to use | Type to import | Purpose |
|---|---|---|
src/.../page_functions.ts | PageFunctions | Types data loading (getProps), static paths, and cache for the page. |
src/.../layout_functions.ts | LayoutFunctions | Types data loading (getProps) and independent cache for the layout shell. |
| Plugins & Shared Utils | RouteSegmentConfig | Base "abstract" interface for generic utilities that process arbitrary route segments. |
import type { PageFunctions } in a page file immediately clarifies intent and self-documents that the functions belong to the page tier.Use getProps to load data on the server based on route params and pass it directly to your component.
[!NOTE] v7 Direct Return Contract: In Dinou v7,
getPropsdirectly returns the props object ({ ... }). The legacy wrapper{ page: { ... }, layout: { ... } }has been deprecated because pages and layouts now have their own dedicated configuration files.
params?: TParams (The normalized dynamic route parameters)TProps | Promise<TProps> (Injected directly as the component's props)// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";
export interface BlogPostData {
post: Post;
}
// Best Practice: Type getProps using PageFunctions for strict parameters & return contract
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
const post = await db.getPost(params?.slug);
// Directly returns BlogPostData for page.tsx
return { post };
};
// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";
// Best Practice: Extend PageProps with the loader's returned BlogPostData
interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}
export default function BlogPost({ params, post }: BlogPostProps) {
return (
<article>
<span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// src/blog/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { db } from "@/lib/db";
export interface BlogLayoutData {
categories: string[];
}
// Best Practice: Type layout server functions with LayoutFunctions
export const getProps: LayoutFunctions<any, BlogLayoutData>["getProps"] = async () => {
const categories = await db.getCategories();
// Directly returns props for layout.tsx
return { categories };
};
// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";
// Best Practice: Extend LayoutProps for automatic typing of children and params
interface BlogLayoutProps extends LayoutProps, BlogLayoutData {}
export default function BlogLayout({
children,
categories,
}: BlogLayoutProps) {
return (
<div className="blog-shell">
<aside className="blog-sidebar">
<h3>Categories</h3>
<ul>
{categories.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
</aside>
<main>{children}</main>
</div>
);
}
getProps Only Receives params & The Dynamic Bailout MechanismRoute params (e.g. /blog/[slug]) identify the resource, not the visitor. Because the content for a given route is identical for everyone, Dinou can run getProps in advance—at build time (SSG) or on first request (ISR)—and safely pre-render and cache the HTML (in static storage, server memory, reverse proxies, or CDNs).
In contrast, cookies, headers, and query params belong to a specific HTTP connection. If getProps relied on them, the output could contain private user data, making shared caching impossible and forcing the server to render every request on demand.
If you call getContext() inside getProps to read request-time connection data:
| Property Accessed | Behavior | Why? |
|---|---|---|
context.request.headers | ❌ Build Error (SSG) / Dynamic Bailout (SSR) | Headers vary per client request. Accessing them invalidates shared static caching. |
context.request.cookies | ❌ Build Error (SSG) / Dynamic Bailout (SSR) | Cookies are user-specific (sessions, auth). Cannot be baked into shared HTML. |
context.searchParams | ❌ Build Error (SSG) / Dynamic Bailout (SSR) | Query strings vary infinitely (?ref=..., ?utm_source=...). Cannot be pre-rendered at build time. |
context.params | ✅ Allowed Everywhere (SSG, ISR, SSR, PPR) | Identifies the route resource ([slug]). Pure and deterministically cacheable. |
To keep pages pre-renderable (SSG/ISR) and instant to load, organize your data into its natural layer: route data in getProps, session cookies inside <Suspense>, and query parameters via the universal useSearchParams() hook:
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";
export interface BlogPostData {
post: Post;
}
// 1. Pure route loader: depends solely on params
// Cached statically for all visitors (SSG / ISR)
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
const post = await db.getPost(params?.slug);
return { post };
};
// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";
import { UserSession } from "./user-session";
import { RelatedPosts } from "./related-posts";
import { SearchFilter } from "./search-filter";
interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}
export default function BlogPostPage({ params, post }: BlogPostProps) {
return (
<article className="post-container">
{/* 1. Instant static shell: delivered immediately from cache */}
<header>
<span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
<h1>{post.title}</h1>
{/* Client Component using useSearchParams() for instant input sync */}
<SearchFilter />
</header>
<div className="post-body">{post.content}</div>
{/* 2. Dynamic cookie component streamed via Suspense */}
<Suspense fallback={<div className="text-xs text-muted-foreground">Loading session...</div>}>
<UserSession />
</Suspense>
{/* 3. Server Component using useSearchParams() streamed via Suspense */}
<Suspense fallback={<div className="text-xs text-muted-foreground">Filtering articles...</div>}>
<RelatedPosts />
</Suspense>
</article>
);
}
// src/blog/[slug]/user-session.tsx (Server Component)
import { getContext } from "dinou";
import { auth } from "@/lib/auth";
// Cookie & auth access isolated inside Suspense:
// Streaming this component prevents getProps from bailing out to Dynamic SSR!
export async function UserSession() {
const { req } = getContext();
const sessionToken = req?.cookies?.session;
const user = sessionToken ? await auth.verify(sessionToken) : null;
if (!user) {
return <a href="/login" className="text-xs font-semibold">Sign In</a>;
}
return (
<aside className="user-badge">
<span>Welcome back, <strong>{user.name}</strong>!</span>
</aside>
);
}
// src/blog/[slug]/related-posts.tsx (Server Component)
import { useSearchParams } from "dinou";
import { db } from "@/lib/db";
// Universal hook in a Server Component:
// Reads ctx.req.query to fetch filtered data from the DB on the server.
// Because it reads query, wrap it in <Suspense> in page.tsx so the post shell stays static!
export async function RelatedPosts() {
const searchParams = useSearchParams();
const tag = searchParams.get("tag") ?? "featured";
const articles = await db.getPostsByTag(tag);
return (
<section className="related-posts">
<h3>Articles matching "?tag={tag}"</h3>
<ul>
{articles.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</section>
);
}
// src/blog/[slug]/search-filter.tsx (Client Component)
"use client";
import { useSearchParams } from "dinou";
// Universal hook in a Client Component:
// Reads URL params on the browser for reactive input state without contacting the server
export function SearchFilter() {
const searchParams = useSearchParams();
const currentTag = searchParams.get("tag") ?? "";
return (
<input
type="search"
defaultValue={currentTag}
placeholder="Filter by tag (?tag=...)..."
className="text-xs border rounded px-2 py-1"
/>
);
}
| Data Layer | Handled In | Lifecycle & Strategy |
|---|---|---|
| 1. Route Data (Params) | getProps in page_functions.ts | Pure and cacheable for all visitors (SSG / ISR). |
| 2. Cookies & Headers | Server Components wrapped in <Suspense> | Streams dynamically without bailing out the static page shell. |
| 3. Universal useSearchParams() | Server or Client Components with <Suspense> | Works in Server Components (streamed) and Client Components (instant UI). |
Defines which dynamic parameter combinations should be pre-rendered at build time (SSG). By default, any dynamic path not returned is generated on-demand when first requested and cached statically for all subsequent visitors (Incremental Static Generation / ISG), unless disabled via allowISG: false (which strictly restricts routes to the pre-rendered list and returns 404 for any other path).
In Dinou v7, getStaticPaths supports both a constant array export (for known, fixed paths) and a synchronous or asynchronous function export (for dynamic data sources like databases or headless CMSs):
// 1. Constant Array Export (Zero runtime overhead, ideal for fixed routes or locales)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = [
"welcome",
"getting-started",
"release-notes",
];
// 2. Async Function Export (Queries databases or headless CMSs at build time)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = async () => {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map((p) => p.slug);
};
Dinou accommodates simple, catch-all, and multi-segment architectures with flexible item formats:
| Route Type | Accepted Format | Example Return | Pre-rendered URLs |
|---|---|---|---|
Simple ([id] or [slug]) | string[] | number[] | ["foo", "bar"] or [1, 2, 3] |
Optional Simple ([[id]]) | `(string | number | undefined)[]` |
Catch-all ([...slug]) | string[][] | [["a", "b"], ["c"]] | /docs/a/b, /docs/c |
Optional Catch-all ([[...slug]]) | `(string[] | undefined)[]` | [undefined, ["a"], ["a", "b"]] |
Multi-Segment / Nested ([cat]/[id]) | `Record<string, string | string[]>[]` | [{ cat: "shoes", id: "100" }] |
Simple dynamic routes match a single URL segment. Dinou supports both required segments ([id]) and optional segments ([[category]]):
// src/blog/[id]/page_functions.ts (Required segment [id])
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[id]">]["getStaticPaths"] = [
"1",
"2",
"3",
]; // Generates: /blog/1, /blog/2, /blog/3
// src/shop/[[category]]/page_functions.ts (Optional segment [[category]])
import type { PageFunctions, RouteParams } from "dinou";
// Pass undefined to pre-render the root route (/shop) without a parameter
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[[category]]">]["getStaticPaths"] = [
undefined, // Generates: /shop
"shoes", // Generates: /shop/shoes
"shirts", // Generates: /shop/shirts
];
Catch-all routes match multiple dynamic path segments. Dinou supports both standard catch-alls ([...slug]) and optional catch-alls ([[...slug]]):
// src/docs/[...slug]/page_functions.ts (Required catch-all [...slug])
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[...slug]">]["getStaticPaths"] = [
["intro"], // Generates: /docs/intro
["api", "v1", "auth"], // Generates: /docs/api/v1/auth
];
// src/docs/[[...slug]]/page_functions.ts (Optional catch-all [[...slug]])
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[[...slug]]">]["getStaticPaths"] = [
undefined, // Generates: /docs (root)
["intro"], // Generates: /docs/intro
["api", "v1", "auth"], // Generates: /docs/api/v1/auth
];
Static parameters automatically propagate downwards through the directory tree. If you define values for a segment, Dinou will recursively generate all nested static sub-pages under those values.
src/blog/[slug]/
├── page.tsx # Page: /blog/[slug]
├── page_functions.ts # Returns ["post-a", "post-b"]
└── details/
└── page.tsx # Nested static page
If getStaticPaths in blog/[slug] returns ["post-a", "post-b"], Dinou generates 4 static pages:
/blog/post-a/blog/post-a/details/blog/post-b/blog/post-b/detailsWhen nesting routes where both the parent and the child segment have their own page.tsx, static generation follows a strict top-down "chain of responsibility":
src/shop/[category]/
├── page.tsx # Parent page: /shop/[category]
├── page_functions.ts # Parent gatekeeper
└── [id]/
├── page.tsx # Child page: /shop/[category]/[id]
└── page_functions.ts # Child generator
Because the parent folder contains a page.tsx, Dinou must generate valid parent pages first before it can traverse down to the child level. The parent acts as a mandatory gatekeeper:
[]: Dinou halts at the parent level and never invokes the child [id]/page_functions.ts. No product pages are generated.// 1. Parent: src/shop/[category]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]">]["getStaticPaths"] = [
"shoes",
"shirts",
];
// 2. Child: src/shop/[category]/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[id]">]["getStaticPaths"] = [
"100",
"200",
];
// Automatically generates 4 static pages:
// /shop/shoes/100, /shop/shoes/200
// /shop/shirts/100, /shop/shirts/200
[!TIP] Rule of Thumb:
- If an intermediate folder has its own
page.tsx, define apage_functions.tsat that folder level with scalar arrays (string[]).- If an intermediate folder does not have a
page.tsx, you can define all nested parameters together in the leafpage_functions.tsusing an Object Array.
When you have multiple dynamic segments in a path without intermediate pages (the intermediate folders do not have a page.tsx), the leaf segment returns an Object mapping all parameter names at once:
src/shop/[category]/[brand]/[...specs]/
└── page.tsx # Only leaf has a page.tsx
// src/shop/[category]/[brand]/[...specs]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[brand]/[...specs]">]["getStaticPaths"] = [
{
category: "electronics",
brand: "apple",
specs: ["m3", "16gb"], // → Pre-renders: /shop/electronics/apple/m3/16gb
},
{
category: "clothing",
brand: "nike",
specs: ["running", "shoes", "size-10"], // → Pre-renders: /shop/clothing/nike/running/shoes/size-10
},
];
getStaticPaths can be an async function, allowing you to query external headless CMSs, databases, or REST APIs during the build:
// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
export const getStaticPaths: PageFunctions<RouteParams<"/products/[id]">]["getStaticPaths"] = async () => {
// Queries your database at build time (npm run build)
const products = await db.products.findMany({ select: { id: true } });
return products.map((product) => String(product.id));
};
Validates dynamic parameters (such as [id] or [...slug]) on the server prior to rendering. If validateParams returns false (or a falsy value), Dinou immediately terminates request processing and issues a 404 Not Found response.
params?: TParams (The normalized route parameters object)boolean | Promise<boolean> (Return false to abort and render 404)// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
type ProductParams = RouteParams<"/products/[id]">;
// 1. Synchronous validation: guarantees id is purely numeric
export const validateParams: PageFunctions<ProductParams>["validateParams"] = (params) => {
return /^[0-9]+$/.test(params?.id ?? "");
};
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
type BlogParams = RouteParams<"/blog/[slug]">;
// 2. Asynchronous validation: verifies record existence before rendering
export const validateParams: PageFunctions<BlogParams>["validateParams"] = async (params) => {
if (!params?.slug) return false;
const exists = await db.posts.exists({ slug: params.slug });
return exists;
};
[!NOTE] Execution Timing:
validateParamsexecutes beforegetProps. If validation fails,getPropswill never run, preventing unnecessary database lookups.
Controls whether dynamic paths that were not pre-rendered at build time (via getStaticPaths during dinou build) are allowed to be generated on-demand on the first incoming user request.
In Dinou v7, pre-rendering happens strictly during the build phase, ensuring production server startup is instantaneous.
true): Any valid path not pre-generated at build time is generated on-demand when the first visitor requests it, and subsequently cached statically (Incremental Static Generation / ISG).false): Strictly restricts accessible routes to those explicitly returned by getStaticPaths. Any request for a path not pre-rendered immediately returns a 404 Not Found without invoking getProps or running SSR.// src/docs/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
// Strict SSG: only these two articles exist; all other /docs/[slug] return 404
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[slug]">]["getStaticPaths"] = [
"installation",
"architecture",
];
// Disallows on-demand generation for unlisted slugs
export const allowISG: PageFunctions["allowISG"] = false;
The revalidate export is Dinou's single source of truth for route caching and rendering behavior. It is measured in seconds (s).
| Revalidate Value | Strategy | Lifecycle & Behavior |
|---|---|---|
revalidate < 1 (or 0) | Dynamic SSR | Escapes static generation. The page is rendered dynamically on every incoming request with live database reads. |
revalidate >= 1 | Incremental Static Regeneration (ISR) | The page is cached statically for N seconds. Requests within the window receive instant cached HTML (0ms TTFB). After N seconds, a background regeneration is triggered. |
revalidate = false | Pure SSG (Default) | The page is rendered once at build time and cached indefinitely until the next build or manual purge. |
// src/metrics/page_functions.ts - Real-time dynamic SSR
import type { PageFunctions } from "dinou";
// Escapes SSG: executes getProps on every request
export const revalidate: PageFunctions["revalidate"] = 0;
// src/store/[id]/page_functions.ts - 60-second ISR
import type { PageFunctions, RouteParams } from "dinou";
export const revalidate: PageFunctions["revalidate"] = 60; // 1 minute ISR
// Conditional revalidation based on environment
export const revalidate: PageFunctions["revalidate"] = () => {
return process.env.NODE_ENV === "development" ? 0 : 3600;
};
Assigns custom tags to the static page cache files, allowing the page to be purged and regenerated on-demand using revalidateTag(tag) (On-Demand Revalidation).
// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
type ProductParams = RouteParams<"/products/[id]">;
// 1. Constant Array
export const getCacheTags: PageFunctions["getCacheTags"] = ["catalog", "products"];
// 2. Synchronous Function (derives tags from route parameters)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = (params) => {
return ["catalog", `product-${params?.id}`];
};
// 3. Asynchronous Function (queries external tags or taxonomy)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = async (params) => {
const categories = await db.categories.forProduct(params?.id);
return ["catalog", ...categories.map((c) => `category-${c.slug}`)];
};
When data changes, call revalidateTag() in a Server Function or webhook to instantly invalidate all cached files tagged with that identifier:
// src/actions/products.ts
"use server";
import { revalidateTag } from "dinou";
import { db } from "@/lib/db";
export async function updateProduct(id: string, data: any) {
await db.products.update({ where: { id }, data });
// Instantly invalidates /products/[id] and any catalog pages sharing the tag
await revalidateTag(`product-${id}`);
}
Partial Prerendering (PPR) combines the instant TTFB (Time to First Byte) of a static page with the real-time capabilities of dynamic server-side rendering over a single HTTP response.
PPR eliminates the traditional trade-off between static and dynamic rendering by dividing the page into two phases over a single HTTP connection:
0ms TTFB) from CDN edge or server cache. Includes layouts, navigation, and fallback skeletons.<Suspense> execute concurrently on the server and stream their resolved HTML chunks directly into the page.// src/dashboard/page_functions.ts
import type { PageFunctions } from "dinou";
// Enables Partial Prerendering for this route segment
export const ppr: PageFunctions["ppr"] = true;
// src/dashboard/page.tsx
import { Suspense } from "react";
import { UserGreeting, UserGreetingSkeleton } from "./user-greeting";
import { LiveMetrics, MetricsSkeleton } from "./live-metrics";
export default function DashboardPage() {
return (
<div className="dashboard-layout">
{/* Static Shell: Pre-rendered at build time (0ms TTFB) */}
<header className="dashboard-header">
<h1>Management Console</h1>
</header>
{/* Dynamic Holes: Streamed progressively over the open HTTP connection */}
<Suspense fallback={<UserGreetingSkeleton />}>
<UserGreeting />
</Suspense>
<Suspense fallback={<MetricsSkeleton />}>
<LiveMetrics />
</Suspense>
</div>
);
}
To achieve complete type safety, autocompletion, and compile-time validation in your routes, import PageFunctions and LayoutFunctions from "dinou" and pass your parameter and props types:
src/blog/[slug]/page_functions.ts// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";
// 1. Strict route parameters derived from URL pattern
export type BlogParams = RouteParams<"/blog/[slug]">;
// 2. Define data contract passed to the component
export interface BlogPageProps {
post: Post;
views: number;
}
// 3. Page configuration with full type inference
export const ppr: PageFunctions["ppr"] = true;
export const revalidate: PageFunctions["revalidate"] = 60; // 1 min ISR
export const getStaticPaths: PageFunctions<BlogParams>["getStaticPaths"] = [
"welcome",
"announcement",
];
export const getProps: PageFunctions<BlogParams, BlogPageProps>["getProps"] = async (params) => {
const post = await db.posts.findUnique({ where: { slug: params?.slug } });
if (!post) throw new Error("Post not found");
return { post, views: 100 };
};
export const validateParams: PageFunctions<BlogParams>["validateParams"] = (params) => {
return Boolean(params?.slug && params.slug.length > 2);
};
export const getCacheTags: PageFunctions<BlogParams>["getCacheTags"] = (params) => {
return ["blog", `post-${params?.slug}`];
};
src/blog/[slug]/page.tsx// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPageProps } from "./page_functions";
import { RealtimeViews, ViewsSkeleton } from "./components";
// Extend PageProps to receive both route params and loader data with full type safety
interface BlogPostPageProps extends PageProps<"/blog/[slug]">, BlogPageProps {}
export default function BlogPostPage({ params, post }: BlogPostPageProps) {
return (
<article className="max-w-2xl mx-auto py-8">
{/* 1. Static shell (pre-rendered at build time, delivered with 0ms TTFB) */}
<span className="text-xs text-muted-foreground font-mono">/blog/{params.slug}</span>
<h1 className="text-3xl font-bold">{post.title}</h1>
<div className="text-sm text-muted-foreground my-2">
By {post.author} • published {post.publishedAt}
</div>
<div className="prose mt-6">{post.content}</div>
{/* 2. PPR Dynamic Stream: Real-time analytics streamed in via Suspense */}
<Suspense fallback={<ViewsSkeleton />}>
<RealtimeViews slug={params.slug} />
</Suspense>
</article>
);
}
src/blog/layout_functions.ts// src/blog/layout_functions.ts
import type { LayoutFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
// 1. Layout parameters for /blog (static route segment without dynamic params = {})
type BlogLayoutParams = RouteParams<"/blog">;
// 2. Define layout data contract
export interface BlogCategory {
id: string;
name: string;
count: number;
}
export interface BlogLayoutData {
categories: BlogCategory[];
featuredSlug: string;
}
// 3. Dedicated layout loader typed via LayoutFunctions
export const getProps: LayoutFunctions<BlogLayoutParams, BlogLayoutData>["getProps"] = async () => {
const categories = await db.categories.findMany();
const featured = await db.posts.findFirst({ where: { featured: true } });
// Injected directly as props into layout.tsx
return {
categories,
featuredSlug: featured?.slug ?? "welcome",
};
};
// 4. Segment-level caching strategy for the layout shell
export const revalidate: LayoutFunctions["revalidate"] = 300; // Layout revalidates every 5 mins
export const ppr: LayoutFunctions["ppr"] = true;
src/blog/layout.tsx// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";
// Extend LayoutProps to receive children, segment params, and layout loader data
interface BlogLayoutComponentProps extends LayoutProps<"/blog">, BlogLayoutData {}
export default function BlogLayout({
children,
categories,
featuredSlug,
}: BlogLayoutComponentProps) {
return (
<div className="blog-container">
<aside className="blog-sidebar">
<h3>Categories</h3>
<ul>
{categories.map((c) => (
<li key={c.id}>
{c.name} ({c.count})
</li>
))}
</ul>
</aside>
<main className="blog-content">
{children}
</main>
</div>
);
}
If you prefer defining all configurations in a single typed object before exporting, use object destructuring:
// 1. Page Config Object in src/blog/[slug]/page_functions.ts
const pageConfig: PageFunctions<BlogParams, BlogPageProps> = {
ppr: true,
revalidate: 60,
getStaticPaths: ["welcome", "announcement"],
getProps: async (params) => {
const post = await fetchPost(params?.slug);
return { post, views: 100 };
},
validateParams: (params) => Boolean(params?.slug),
getCacheTags: (params) => ["blog", `post-${params?.slug}`],
};
export const { ppr, revalidate, getStaticPaths, getProps, validateParams, getCacheTags } = pageConfig;
// 2. Layout Config Object in src/blog/layout_functions.ts
const layoutConfig: LayoutFunctions<any, BlogLayoutData> = {
revalidate: 300,
ppr: true,
getProps: async () => ({
categories: await fetchCategories(),
featuredSlug: "welcome",
}),
};
export const { revalidate, ppr, getProps } = layoutConfig;
Dinou is Static by Default. It provides a continuum of rendering strategies—from build-time SSG and stale-while-revalidate ISR to real-time Dynamic SSR, revolutionary Partial Prerendering (PPR), and component-level Micro-ISR with <DinouCacheSlot>.
In Dinou, every route is assumed to be static until proven otherwise. During dinou build, the compiler analyzes route segments, components, and loader functions to evaluate whether a route can be pre-rendered or requires real-time request data:
revalidate: 0, Dinou automatically bails out to Live SSR on every incoming request.<DinouCacheSlot>.Pure static rendering pre-generates HTML and RSC files during the build phase (npm run build). Production server startup is instantaneous, and static pages achieve the fastest possible Time to First Byte (TTFB) cached by Edge CDNs globally.
Any page that does not consume request-specific headers, cookies, or query parameters and does not set revalidate < 1 is automatically generated as static HTML:
// src/about/page.tsx
// Automatically rendered as static HTML during build time ('npm run build')
export default function AboutPage() {
return (
<article className="prose">
<h1>About Us</h1>
<p>This page is pre-rendered once and served statically forever.</p>
</article>
);
}
If a Server Component reads request-specific data at render time without wrapping it inside a dynamic streaming boundary (React <Suspense> with Partial Prerendering / PPR enabled), Dinou automatically "bails out" of static generation and transitions the entire page into Dynamic SSR for that request:
// src/profile/page.tsx
import { getContext } from "dinou";
import db from "@/lib/db";
export default async function ProfilePage() {
const context = getContext();
// ⚠️ Reading cookies outside <Suspense> triggers an Automatic Static Bailout
// This page will now be rendered dynamically on every incoming request
const sessionToken = context?.req.cookies["session_token"];
const user = await db.users.findByToken(sessionToken);
return <h1>Welcome back, {user?.name}</h1>;
}
Automatic bailout occurs when reading context.req.cookies, context.req.headers, or context.req.query (URL search parameters) during server component execution without wrapping the access in a React <Suspense> boundary.
useSearchParams()?Client Components ("use client") that call useSearchParams() do not trigger dynamic bailout. The page remains 100% static (SSG). During build time, the component is pre-rendered with an empty parameter set, and live URL parameters are synchronized in the browser upon hydration.
[!TIP] Preventing Hydration Mismatches with Search Parameters:
If a Client Component immediately renderssearchParams.get("q")in its initial JSX markup, the server-generated static HTML (where the query was empty at build time) will differ from the live browser DOM (e.g.?q=react), triggering a React hydration mismatch error.Recommended Fix: Keep initial render deterministic, then read and apply live search parameters inside
useEffect()after mounting:// src/components/search-filter.tsx "use client"; import { useSearchParams } from "dinou"; import { useState, useEffect } from "react"; export function SearchFilter() { const searchParams = useSearchParams(); const [query, setQuery] = useState<string | null>(null); useEffect(() => { // Synchronize client-side only after mounting -> eliminates hydration mismatch setQuery(searchParams.get("q") ?? ""); }, [searchParams]); if (query === null) { return <span className="text-muted-foreground">Loading filter...</span>; } return <span>Filter: <strong>{query}</strong></span>; }Alternative (Live SSR): If the initial HTML sent over the wire must contain search parameters for SEO, switch the page to Live SSR by exporting
export const revalidate = 0;inpage_functions.ts.
revalidate Strategy (Strict Seconds)In Dinou v7, the revalidate export in page_functions.ts (or layout_functions.ts) serves as the single source of truth for route rendering behavior. It is measured strictly in seconds (s):
Value of revalidate | Strategy | Description |
|---|---|---|
false (or omitted) | Pure Static (SSG) | Pre-rendered once at build time. Cached indefinitely until manual invalidation. |
0 (or number < 1) | Dynamic SSR | Rendered on every incoming HTTP request on the fly. |
N (number >= 1) | Time-Based ISR | Pre-rendered, served instantly from cache, and revalidated in the background every N seconds. |
revalidate < 1 or 0)To force a page to render dynamically on every single request (escaping the default static generation), set revalidate to 0:
// src/analytics/live/page_functions.ts
// Escapes static generation: executes dynamic SSR on every request
export const revalidate = 0;
[!NOTE] Migration from Legacy
dynamicExport: In older versions of Dinou, dynamic rendering was toggled viaexport function dynamic(). In Dinou v7, thedynamicexport has been completely removed. Settingexport const revalidate = 0;is the official unified mechanism to escape static generation.
revalidate >= 1)When revalidate >= 1, Dinou serves pre-rendered static HTML/RSC payloads while regenerating the page in the background at most once every N seconds (stale-while-revalidate):
// src/blog/page_functions.ts
// Rebuilds the cached page in the background at most once every 60 seconds
export const revalidate = 60; // Strict seconds (1 minute)
DenoKVStorage, CloudflareKVStorage, MemoryStorage) without needing a persistent disk.Partial Prerendering (PPR) resolves the decades-old dilemma between static and dynamic rendering. Instead of forcing an entire page to be 100% static or 100% dynamic, PPR delivers a pre-rendered static shell instantly (0ms TTFB), while streaming dynamic holes in parallel over the same HTTP response.
At build time, Dinou compiles the layout, navigation, and static components into an immutable HTML shell. Any dynamic component wrapped in React <Suspense> is saved as a streaming placeholder hole (<div data-ppr-hole="slot-id">...fallback...</div>).
<Suspense> on the server.useEffect() + fetch() anti-patterns, suffering waterfalls, layout shifts, and spinners.0ms TTFB from the edge, while the component inside <Suspense> executes on the server on every incoming request. You get 100% fresh, live database data streamed directly to the browser without client fetch waterfalls.Without PPR, dynamic data access is infectious: if a single nested component reads request cookies or headers, Dinou triggers an Automatic Static Bailout for the entire page, destroying static pre-rendering and edge caching for the whole route.
With PPR enabled (ppr = true), React <Suspense> acts as an isolated sandbox. Dynamic data consumption inside the boundary does not escape to parent or sibling components:
// src/store/page.tsx
import { Suspense } from "react";
import { getContext } from "dinou";
import db from "@/lib/db";
import { ProductGallery } from "@/components/gallery";
import { CartBadgeSkeleton } from "@/components/skeletons";
// ⚠️ Dynamic component: reads request-specific session cookie
async function LiveCartBadge() {
const context = getContext();
const sessionToken = context?.req.cookies["session_id"];
const cart = await db.carts.findActive(sessionToken);
return <div className="cart-badge">Items: {cart?.itemCount ?? 0}</div>;
}
export default function StorePage() {
return (
<main className="container">
{/* 1. Static Shell: 0ms TTFB, pre-rendered during build */}
<header className="navbar">
<h1>Flagship Store</h1>
{/* 2. PPR Boundary: Isolates cookie bailout exclusively to this hole */}
<Suspense fallback={<CartBadgeSkeleton />}>
<LiveCartBadge />
</Suspense>
</header>
{/* 3. Static Sibling: Remains 100% static and edge-cached */}
<ProductGallery />
</main>
);
}
cookies["session_id"] turns the entire page into dynamic SSR. Edge caching is lost, and the visitor waits with a blank screen.<Suspense>. The navbar and product gallery are delivered instantly at 0ms TTFB. Only the cart badge executes on the server and streams into place.Dinou provides granular control over Partial Prerendering across three hierarchical levels:
page_functions.ts) — Highest Priority: A page can override any parent layout or global configuration:
// src/shop/products/[slug]/page_functions.ts
export const ppr = true; // Guarantees PPR on product details
layout_functions.ts) — Subtree Cascading: Applies to that layout and cascades to all its descendant subroutes:
// src/admin/layout_functions.ts
export const ppr = false; // Disables PPR for /admin and ALL subroutes
dinou.config.mjs): Enables PPR globally across all eligible routes:
// dinou.config.mjs
import { defineConfig } from "dinou";
export default defineConfig({ ppr: true });
<DinouCacheSlot>)Traditional full-stack frameworks only offer Horizontal Segmentation (caching an entire URL route or layout shell). When a dynamic page contains an expensive database widget, developers are often forced into an architectural trap: converting the widget to a Client Component with client-side cache libraries (SWR, TanStack Query), introducing bundle bloat, waterfall requests, and layout shifts.
Dinou v7 introduces Vertical Segmentation with <DinouCacheSlot> from "dinou/server". It keeps components securely on the server as React Server Components (RSC) while caching their rendered serialized Flight payload in server storage with dedicated TTLs and invalidation tags.
<DinouCacheSlot> functions as a universal caching primitive across two core architectural patterns:
The route runs in Live SSR mode (evaluating user cookies and personal greetings live). The heavy analytics calculation is cached on the server for 300 seconds and wrapped in React <Suspense>:
// src/dashboard/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { FastUserProfile, SlowAnalyticsWidget } from "./widgets";
import { AnalyticsSkeleton } from "./skeletons";
export default function DashboardPage() {
return (
<main className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
{/* 1. Dynamic Component: Evaluated live on every request (reads session/cookies) */}
<FastUserProfile />
{/* 2. Micro-ISR Slot: Cached on the server for 300 seconds */}
{/* ⚠️ Wrapped in <Suspense> so cache misses stream without blocking TTFB */}
<Suspense fallback={<AnalyticsSkeleton />}>
<DinouCacheSlot id="analytics-widget" tag="analytics" revalidate={300}>
<SlowAnalyticsWidget />
</DinouCacheSlot>
</Suspense>
</main>
);
}
The parent route has a slow horizontal revalidation of 24 hours (revalidate: 86400). Inside it, a volatile inventory counter revalidates vertically every 60 seconds (revalidate={60}) or on-demand:
// src/products/[slug]/page_functions.ts
export const revalidate = 86400; // Slow horizontal revalidation (24h)
// src/products/[slug]/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { ProductHero, ProductDescription } from "./static-widgets";
import { LiveStockCounter, StockSkeleton } from "./stock-widgets";
export default function ProductDetailPage({ params }: { params: { slug: string } }) {
return (
<main className="container py-8 space-y-6">
{/* 1. Heavy Static Content: Cached at the CDN edge for 24 hours */}
<ProductHero slug={params.slug} />
<ProductDescription slug={params.slug} />
{/* 2. Asymmetric Micro-ISR Slot: Revalidates vertically every 60s */}
<Suspense fallback={<StockSkeleton />}>
<DinouCacheSlot
id={`stock-${params.slug}`}
tag={`stock-${params.slug}`}
revalidate={60}
>
<LiveStockCounter slug={params.slug} />
</DinouCacheSlot>
</Suspense>
</main>
);
}
[!IMPORTANT] Boundary Placement: Why
<Suspense>Must Wrap<DinouCacheSlot>
<DinouCacheSlot>is itself an asynchronous Server Component. When it awaits storage or renders on a cache miss, React encounters a pending promise in the parent page tree. Without an outer<Suspense>, suspension bubbles to the root, blocking the entire HTTP response and delaying fast sibling components.The Rule of Hierarchy:
<Suspense fallback={<Skeleton />}>→ Controls browser streaming & instant visual layout.<DinouCacheSlot revalidate={300}>→ Controls server cache storage, TTL, and cache tags.<SlowAnalyticsWidget />→ Executes database queries and business logic.
Dinou provides specialized mechanisms to refresh or purge <DinouCacheSlot> instances without reloading the browser page:
refreshSlot)Any interactive Client Component can trigger a surgical re-render of only that slot without full page reload or state loss:
"use client";
import { refreshSlot, useRouter } from "dinou";
export function RefreshButton() {
const router = useRouter();
return (
<div className="flex gap-2">
{/* Option 1: Standalone (respects server slot TTL) */}
<button onClick={() => refreshSlot("analytics-widget")}>
Refresh Metrics
</button>
{/* Option 2: Force fresh (bypasses server slot TTL immediately) */}
<button onClick={() => router.refreshSlot("analytics-widget", { fresh: true })}>
Force Fresh
</button>
</div>
);
}
refreshSlot(id) (Default): Refreshes the client DOM with the server's current state. If the slot's server TTL has not expired, it returns cached server JSX without touching your database (protecting against button spam and traffic spikes).refreshSlot(id, { fresh: true }) (Bypass): Hard revalidation. Forces the server to ignore unexpired TTL, immediately re-run queries against the database, and update both cache and client.revalidateTag)Mutations purge slots globally via revalidateTag() on the server, while the client immediately pulls the fresh result via refreshSlot():
// src/app/feed/actions.ts
"use server";
import { revalidateTag } from "dinou/server";
import db from "@/lib/db";
export async function publishPost(formData: FormData) {
const content = formData.get("content") as string;
await db.posts.insert({ content, createdAt: new Date() });
// Purges cache globally for ALL future visitors across CDNs and Micro-ISR slots
await revalidateTag("feed");
}
startTransition + Stale-Preserving Fallback)While the server re-renders an expired slot during an interactive refresh, the user does NOT see a blank skeleton flash or layout shift. Dinou's internal client boundary wraps slot updates in React.startTransition and sets fallback: children (the previous DOM content). The existing widget stays interactive and visible until the new server stream arrives, then swaps seamlessly.
To display an inline loading indicator on the button itself, wrap refreshSlot inside React's standard useTransition():
"use client";
import { useTransition } from "react";
import { refreshSlot } from "dinou";
export function RefreshAnalyticsButton() {
const [isPending, startTransition] = useTransition();
const handleRefresh = () => {
startTransition(() => {
refreshSlot("analytics-widget");
});
};
return (
<button onClick={handleRefresh} disabled={isPending}>
{isPending ? "Refreshing Metrics..." : "Refresh Metrics"}
</button>
);
}
| Dimension | Scope | Configuration | Best For |
|---|---|---|---|
| Horizontal Segmentation | Route / Segment Level | page_functions.ts, layout_functions.ts | Whole-page SSG, ISR, and layout persistence. |
| Vertical Segmentation | Component Subtree Level | <DinouCacheSlot> from dinou/server | Independent widget TTLs, asymmetric revalidation, and in-place client refreshes. |
<DinouCacheSlot>page.tsx body inside a slot reinvents horizontal caching. Declare export const revalidate = 300 in page_functions.ts instead.<DinouCacheSlot>Nesting <DinouCacheSlot> inside a React <Suspense> boundary on a route with Partial Prerendering (PPR) unlocks the highest performance profile possible: zero-delay initial delivery (0ms TTFB) paired with near-zero streaming delay (~2ms) and 99.9% database query offloading.
<DinouCacheSlot> (Cached Streamed Hole): 0ms static shell from Edge CDN + hole resolves from server cache in ~2ms. 10,000 visitors/min = 1 database query per TTL window (99.9% database offload).// src/telemetry/page_functions.ts
import type { PageFunctions } from "dinou";
// Enable Partial Prerendering on this high-traffic route
export const ppr: PageFunctions["ppr"] = true;
// src/telemetry/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { TelemetryHeader, TelemetryFooter } from "./static-shell";
import { HeavyMetricsGraph, GraphSkeleton } from "./graph-widgets";
export default function TelemetryPage() {
return (
<main className="container py-8 space-y-6">
{/* 1. Static Shell: Delivered instantly in 0ms TTFB from Edge CDN */}
<TelemetryHeader />
{/* 2. PPR Dynamic Hole + Micro-ISR Slot:
- Fallback skeleton sent immediately in initial layout
- Hole streams from server cache in ~2ms for subsequent visitors
- Database query runs only ONCE every 60 seconds */}
<Suspense fallback={<GraphSkeleton />}>
<DinouCacheSlot id="operations-telemetry" tag="telemetry" revalidate={60}>
<HeavyMetricsGraph />
</DinouCacheSlot>
</Suspense>
<TelemetryFooter />
</main>
);
}
While ISR updates existing pre-rendered pages, ISG (Incremental Static Generation) generates new dynamic paths on demand that were omitted from the build-time getStaticPaths list.
Any route path omitted from getStaticPaths will be generated dynamically when the first visitor requests it, cached, and then served as static HTML to subsequent visitors:
// src/products/[id]/page_functions.ts
// Pre-render only the top 10 products at build time:
export const getStaticPaths = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
// Any other ID (e.g. /products/452) will be generated on-demand
// on its first visitor and cached permanently.
validateParams & allowISG)Dinou provides strict server-side governance exports to secure on-demand generation against cache pollution and DDoS:
validateParams(params): Validates route parameters before generation begins. Returning false halts rendering immediately and serves an instant 404:
export function validateParams(params: { id: string }) {
return /^\d+$/.test(params.id); // Only allow numeric IDs
}
allowISG: Disables on-demand generation entirely. When set to false, any path not pre-rendered in getStaticPaths returns 404 without querying the server:
export const allowISG = false; // Strict SSG only
Summary of all rendering strategies supported natively by Dinou v7:
| Strategy | How to Configure | TTFB | Freshness | Database Load | Best For |
|---|---|---|---|---|---|
| Pure SSG | Default / revalidate = false | 0ms (Instant) | Build-time | 0 queries (Build-time only) | Landing pages, marketing, documentation |
| ISR | revalidate = N (N >= 1) | 0ms (Cached) | Stale-while-revalidate | 1 query per TTL (0 on cache hits) | Blogs, catalogs, news feeds |
| Dynamic SSR | revalidate = 0 | Request-time (Blocked) | 100% Real-time | Heavy: 100% queries on every request | User settings, banking, checkout |
| PPR (Partial Prerendering) | ppr = true + <Suspense> | 0ms (Shell from Edge) | 100% Real-time (Holes) | Heavy: 100% Hole queries on every request | E-commerce, dashboards with static shells |
| Micro-ISR (Static / Asymmetric ISR) | <DinouCacheSlot> (in Static/ISR page) | 0ms (CDN Edge) | Decoupled Slot TTL / Tags | Low: Isolated to expired slots | Static product/catalog pages with volatile widgets (stock, prices) |
| Micro-ISR (Dynamic SSR) | <DinouCacheSlot> (in Dynamic SSR page) | Fast (Early Layout / Slot-cached) | Component TTL / Tags | Low: Isolated to expired slots | Dynamic user dashboards with heavy SQL calculations |
| PPR + Micro-ISR (Synergy) | ppr = true + <DinouCacheSlot> | 0ms (Edge) + ~2ms (Hole) | Cached Streamed Hole | Near-Zero: 1 query per slot TTL (99.9% reduction) | High-concurrency e-commerce, telemetry dashboards |
| ISG (Incremental Static) | allowISG = true (default) | 0ms (after 1st hit) | Cached permanently | 1 query on 1st generation (0 thereafter) | Long-tail e-commerce products, dynamic paths |
[!TIP] For deep-dive code recipes, runtime execution timing, TTFB charts, and production trade-offs across all 13 rendering and data fetching patterns, see Data Fetching: The 13 Strategies below.
Server Functions are asynchronous RPC endpoints that run exclusively in the server runtime. In Dinou, you declare them by adding the "use server" directive at the top of a dedicated action file (module-level directive).
| Capability | Architectural Benefit |
|---|---|
| Zero API Boilerplate | No manual route handlers, fetch() boilerplate, or JSON serialization required. Dinou generates typed Flight RPC endpoints (POST /____server_function____) automatically. |
| JSX & Data Returns | Unlike standard RPCs that only return JSON, Dinou Server Functions can return fully rendered React components streamed over Flight RPC (The Dinou Pattern). |
| Security & Secrets | Secret keys, database connections, and ORM calls are never bundled into the client-side JavaScript chunk. |
| Form & Action Synergy | Native integration with HTML <form action={...}> and React 19 primitives (useActionState, useFormStatus, useOptimistic). |
Dinou uniquely allows Server Functions to return fully rendered React component trees directly over Flight RPC. In Server Components, the component is awaited directly inside native <Suspense>; in Client Components, the returned promise is unwrapped with React 19's use() hook:
// src/actions/get-post-card.tsx
"use server"; // 👈 Module-level directive in Dinou
import db from "@/lib/db";
import { PostCard } from "@/components/post-card";
export async function getPostCard(postId: string) {
const post = await db.posts.findUnique({ where: { id: postId } });
if (!post) throw new Error("Post not found");
// Returns a rendered JSX component directly over Flight RPC!
return <PostCard post={post} />;
}
Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve:
// src/blog/page.tsx
// Server Component: directly await the Server Function with React 19 Suspense
import { Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";
async function PostSlot({ id }: { id: string }) {
// Directly awaited on the server during SSR / streaming
return await getPostCard(id);
}
export default function BlogPage() {
return (
<section className="space-y-4">
<h1 className="text-2xl font-bold">Featured Story</h1>
{/* Native React 19 Suspense fallback */}
<Suspense fallback={<div className="skeleton h-48 w-full rounded" />}>
<PostSlot id="post-101" />
</Suspense>
</section>
);
}
use())Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries, and re-suspends cleanly on promise update:
// src/blog/page.tsx
// Client Component: unwrap the Flight RPC promise with React 19 use()
"use client";
import { useState, use, Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";
function PostStream({ promise }: { promise: Promise<React.ReactNode> }) {
// Pure React 19 'use()' unrolls the promise within Suspense
const content = use(promise);
return <>{content}</>;
}
export default function BlogPage() {
const [postId, setPostId] = useState("post-1");
const [postPromise, setPostPromise] = useState(() => getPostCard("post-1"));
const loadPost = (id: string) => {
setPostId(id);
setPostPromise(getPostCard(id));
};
return (
<section className="space-y-4">
<h1 className="text-2xl font-bold">Featured Story</h1>
<div className="flex gap-2">
<button onClick={() => loadPost("post-1")} className="btn">Post 1</button>
<button onClick={() => loadPost("post-2")} className="btn">Post 2</button>
</div>
{/* Re-suspends cleanly on promise update */}
<Suspense key={postId} fallback={<p>Streaming post from server...</p>}>
<PostStream promise={postPromise} />
</Suspense>
</section>
);
}
[!NOTE] Execution Comparison: Server vs. Client Components
- In Server Components (Async / Await): Directly
awaitthe Server Function inside an async component. React streams HTML progressively as promises resolve.- In Client Components (with
use()): Dispatches Flight RPC returning a Promise. React 19's nativeuse(promise)unrolls it synchronously inside<Suspense>without external state libraries.
Dinou integrates seamlessly with standard HTML forms and React 19 form actions for data mutations.
You can pass a Server Action directly to the native HTML <form action={...}> attribute:
// src/components/newsletter-page.tsx
"use client";
import { subscribeNewsletter } from "@/actions/subscribe";
export default function NewsletterPage() {
return (
<form action={subscribeNewsletter} className="flex gap-2">
<input name="email" type="email" placeholder="[email protected]" required className="input" />
<button type="submit" className="btn-primary">Subscribe</button>
</form>
);
}
// src/actions/subscribe.ts
"use server"; // 👈 Required at the module level in Dinou
import db from "@/lib/db";
export async function subscribeNewsletter(formData: FormData) {
const email = formData.get("email") as string;
await db.subscribers.create({ data: { email } });
}
[!NOTE] Module-Level Directives & Client Execution Dinou requires the
"use server"directive to be placed at the top of the action file (not inline within function bodies). When a form submits, Dinou's client runtime transparently dispatches a Flight RPC request (POST /____server_function____) to execute the server action and receive updated UI state.
[!TIP] What does
subscribeNewsletterreturn? (Understandingvoid/undefined) This action returnsPromise<void>, which resolves toundefined. In pure React 19 form actions (<form action={subscribeNewsletter}>), this is completely standard for fire-and-forget mutations: the browser submits the form data, the server executes the database write, and no state payload is needed by the UI.When should a Server Action return a value? If your UI needs to display interactive feedback (such as "Subscribed successfully!" or validation errors), the action should return a serializable payload (e.g.
{ success: true, message: "..." }) and be wired to React 19'suseActionStatehook (as shown below in Section 2.2). Other common patterns include performing a server-sideredirect()or callingrevalidatePath()before completing.
For rich interactive forms with validation feedback and loading spinners, use React 19's standard hooks:
useActionState (React 19): Captures the returned state (errors, success payload) and pending status of the Server Action across submissions.useFormStatus (react-dom): Reads the pending submission status of the parent <form> without prop drilling.// src/components/create-post-form.tsx
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createPostAction, type FormState } from "@/actions/posts";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className="btn-primary">
{pending ? "Saving..." : "Publish Post"}
</button>
);
}
const initialState: FormState = { success: false, message: "" };
export function CreatePostForm() {
// useActionState binds the server action to form state and tracks submission
const [state, formAction, isPending] = useActionState(createPostAction, initialState);
return (
<form action={formAction} className="flex flex-col gap-4">
{state.message && (
<div className={state.success ? "text-green-500" : "text-red-500"}>
{state.message}
</div>
)}
<input name="title" placeholder="Post title" required className="input" />
<textarea name="content" placeholder="Write content..." required className="textarea" />
<SubmitButton />
</form>
);
}
// src/actions/posts.ts
"use server"; // 👈 Module-level directive required in Dinou
import db from "@/lib/db";
export type FormState = {
success: boolean;
message: string;
};
// Server Action receives (prevState, formData) when bound to useActionState
export async function createPostAction(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const title = (formData.get("title") as string)?.trim();
const content = (formData.get("content") as string)?.trim();
if (!title || title.length < 3) {
return { success: false, message: "Title must be at least 3 characters." };
}
if (!content) {
return { success: false, message: "Content cannot be empty." };
}
await db.posts.create({ data: { title, content } });
return { success: true, message: "Post published successfully!" };
}
With React 19's useOptimistic, you can update the user interface immediately before the server responds:
// src/components/like-button.tsx
"use client";
import { useOptimistic, useTransition } from "react";
import { toggleLikeAction } from "@/actions/likes";
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [isPending, startTransition] = useTransition();
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
initialLikes,
(current, update: number) => current + update
);
const handleClick = () => {
startTransition(async () => {
setOptimisticLikes(1); // Optimistically increment in 0ms on client
await toggleLikeAction(postId); // Dispatches Server Action via Flight RPC
});
};
return (
<button onClick={handleClick} disabled={isPending} className="btn-like">
❤️ {optimisticLikes}
</button>
);
}
// src/actions/likes.ts
"use server"; // 👈 Module-level directive required in Dinou
import db from "@/lib/db";
export async function toggleLikeAction(postId: string) {
// Updates like count on the database
await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
return { success: true };
}
[!NOTE] Why is there no
<Suspense>boundary here?<Suspense>only activates when a component suspends (by throwing a pending Promise, such as withReact.use(promise)or an async Server Component).In contrast,
useOptimisticdoes the exact opposite of suspending: it synchronously paints the temporary new value (e.g. switching immediately from10to11) in 0ms on the client, avoiding loading spinners or skeleton fallbacks entirely while the Server Action runs in the background. It relies strictly on React 19 transitions (useTransitionor Form Actions). If the server call succeeds, React seamlessly commits the real server state; if it fails, React automatically rolls back to the initial value with zero boilerplate.
Dinou provides getContext() (exported from "dinou") to access the underlying request and response lifecycle synchronously within any Server Function or SSR component.
RequestContextStore API Reference:
context.req.cookies: Dictionary of incoming client cookies (e.g. cookies["session_token"]).context.req.headers: Incoming HTTP headers dictionary (e.g. headers["user-agent"]).context.req.query: URL search parameters.context.res.cookie(name, value, options): Sets an HTTP cookie with options (httpOnly, secure, maxAge, sameSite, path, etc.).context.res.clearCookie(name, options): Clears a cookie from the client.// src/actions/auth-check.ts
"use server";
import { getContext } from "dinou";
export async function getCurrentUser() {
const context = getContext();
if (!context) return null;
const sessionToken = context.req.cookies["session_token"];
const userAgent = context.req.headers["user-agent"];
if (!sessionToken) return null;
return await verifySession(sessionToken);
}
// src/actions/login.ts
"use server";
import { getContext } from "dinou";
export async function loginAction(formData: FormData) {
const context = getContext();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const session = await authenticateUser(email, password);
// Set secure HTTP-only session cookie
context?.res.cookie("session_token", session.token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
path: "/",
});
return { success: true };
}
export async function logoutAction() {
const context = getContext();
// Clear the cookie upon logout
context?.res.clearCookie("session_token", { path: "/" });
return { success: true };
}
Dinou provides a universal redirect(destination) utility exported from "dinou":
router.replace()) without reloading the page.// src/actions/checkout.ts
"use server";
import { redirect } from "dinou";
import db from "@/lib/db";
export async function completeOrder(orderId: string) {
await db.orders.updateStatus(orderId, "PAID");
// Automatically navigates the client router to the success page
return redirect(`/checkout/${orderId}/success`);
}
A foundational question when handling redirects across hybrid architectures is: How does Dinou determine whether redirect() issues an HTTP 302 vs. a client-side transition, and is calling redirect() in a Server Component equivalent to invoking it via a Server Function?
[!NOTE] The Equivalence Principle in Server Components Calling
redirect()directly inside a Server Component vs. calling an imported Server Function (e.g.,await verifyUser()) from that Server Component is functionally identical. Because both run inside the same server Node.js process during the same SSR request lifecycle, they share the exact same request context and streaming state.
Occurs inside getProps() in page_functions.ts, at the top level of the root page.tsx Server Component (outside any <Suspense> boundary), or in a global plugin hook (onRequest) in dinou.config.mjs. Because zero HTML bytes have reached the wire (headersSent === false), Dinou issues a clean HTTP 302 with a Location header. Crucial for SEO crawlers and search indexers.
// src/dashboard/page_functions.ts
import { redirect, getContext } from "dinou";
export async function getProps() {
const { req } = getContext();
if (!req.cookies.session_token) {
// Evaluated before page rendering -> HTTP 302 redirect
return redirect("/login");
}
return { user: { name: "Alex" } };
}
// src/dashboard/page.tsx (Server Component root)
import { redirect, getContext } from "dinou";
export default async function DashboardPage() {
const { req } = getContext();
if (!req.cookies.session_token) {
// At root before stream begins -> HTTP 302 redirect
return redirect("/login");
}
return <h1>Welcome to Dashboard</h1>;
}
// dinou.config.mjs (Global Plugin Hook)
import { defineConfig } from "dinou/config";
export default defineConfig({
plugins: [
{
name: "auth-guard",
async onRequest(request, context) {
const url = new URL(request.url);
const cookieHeader = request.headers.get("cookie") || "";
// Runs pre-flight before SSR or route resolution
if (url.pathname.startsWith("/dashboard") && !cookieHeader.includes("session_token=")) {
return Response.redirect(new URL("/login", request.url), 302);
}
},
},
],
});
Occurs inside an async child wrapped in <Suspense fallback={<Skeleton />}>. React opens the HTTP connection with status 200 OK immediately to stream the initial shell. At HTTP protocol level, an open 200 OK stream cannot turn into a 302. Dinou safely emits <ClientRedirect />, completing the transition upon browser hydration without breaking the stream:
// src/reports/page.tsx (Server Component)
import { Suspense } from "react";
import { redirect } from "dinou";
async function RestrictedReport() {
const isAllowed = await checkUserAccess();
if (!isAllowed) {
// 200 OK stream already open -> <ClientRedirect />
return redirect("/unauthorized");
}
return <div>Financial Metrics</div>;
}
export default function Page() {
return (
<Suspense fallback={<p>Loading financial report...</p>}>
<RestrictedReport />
</Suspense>
);
}
Triggered from browser interactions (form actions or button clicks) via a dedicated POST request. Dinou executes the server mutation and intercepts redirect() before streaming (headers unsent), returning redirect metadata. The client proxy navigates via the SPA router without a full page reload:
// src/actions/cart.ts
"use server";
import { redirect } from "dinou";
import db from "@/lib/db";
export async function checkoutAction(formData: FormData) {
const order = await db.orders.create({ /* ... */ });
// Dedicated POST: soft SPA navigation in browser without page reload
return redirect(`/orders/${order.id}`);
}
Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms:
// src/components/client-guard.tsx
"use client";
import { redirect } from "dinou";
export function ClientGuard({ isAllowed }: { isAllowed: boolean }) {
if (!isAllowed) {
// In-browser render: executes router.replace() in SPA router
return redirect("/forbidden");
}
return <button className="btn">Control Panel</button>;
}
| Invocation Context | Headers Sent? | Resulting Mechanism | Recommended For |
|---|---|---|---|
| 1. SSR Root ( page_functions.ts, root page.tsx, Plugin) | No | Hard HTTP Redirect (Status 302) Server issues an HTTP/1.1 302 Found with a Location header. The browser or search crawler intercepts it at the protocol level and fetches the new page as a fresh document. | Auth guards (getProps), root guards, global middleware plugins, SEO canonicals |
| 2. SSR: Deferred ( <Suspense>) | Yes (200 OK stream open) | Streaming Fallback (<ClientRedirect />)Because the 200 OK chunked stream cannot be rewritten to a 302, Dinou injects <ClientRedirect /> into the stream. The browser router transitions upon hydration without aborting the connection. | Deferred conditional access, late data-dependent checks inside Suspense |
| 3. Server Function (from Client Action) | No (standard POST) | Soft SPA Navigation (Status 200) Server returns HTTP 200 OK with redirection metadata. Dinou's client runtime intercepts it and navigates via the SPA router without a full page reload or loss of client state. | Form submissions, shopping cart checkout, stateful mutations |
| 4. Client Component (Render) | N/A (in-browser) | Client Router Replacement (router.replace())Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms. | Pure UI state redirection, client-side permission gates |
[!TIP] Looking for Cache Revalidation? Learn how to purge stale SSG pages and ISR cache slots on-demand using
revalidatePathandrevalidateTagin On-Demand Revalidation below.
High-performance web applications cannot rely exclusively on time-based expiration (TTL). On-Demand Revalidation in Dinou enables instantaneous cache purging and static regeneration across four specialized tiers:
| Invalidation Tier | API & Mechanism | Architectural Scope |
|---|---|---|
| Surgical Page | revalidatePage(path) | Regenerates exclusively page.rsc and index.html, leaving all parent layout shells completely untouched. |
| Route Segment | revalidatePath(path) | Purges page RSC, HTML, and any layout.rsc in that segment. Supports recursive propagation with { cascade: true }. |
| Cross-Cutting Tag | revalidateTag(tag) | Selectively updates layouts, pages, or micro-slots across disparate routes sharing a common metadata tag key. |
| Micro-Slot | <DinouCacheSlot> | Isolates volatile component subtrees refreshed independently via refreshSlot() or targeted revalidateTag(). |
Server revalidation APIs are exported from the server-only endpoint "dinou/server", keeping browser client bundles clean, lightweight, and secure.
[!CAUTION] Strict Execution Rules: Where to Call Revalidation APIs
- Only in Mutative Event Contexts: Revalidation APIs (
revalidatePage,revalidatePath,revalidateTag) are mutative side-effects. They must only be invoked inside Server Functions ("use server") or HTTP Webhook endpoints (such as Headless CMS publishing events).- Never Inside Components or
getProps: Calling revalidation during the component render phase or withingetPropscreates an infinite regeneration loop, causing severe CPU exhaustion and server crashes.
Exported from "dinou/server", revalidatePage(path) is specifically designed for scenarios where only the leaf page content has changed. It provides two critical advantages:
Truncated — view the full README on GitHub.
Full-Stack React 19 Framework
See the codeThe ejectable React 19 framework built on web standards. Build full-stack applications with native React Server Components (RSC), progressive streaming SSR, compile-time type-safe routing, dual-dimensional caching, and universal multi-platform deployment (Node.js, Bun, Deno, Cloudflare Workers, Netlify, and pure static hosts) with zero vendor lock-in.
renderToReadableStream) directly into the DOM with instant shell flushing."react-server" vs. "browser") within a single in-process pipeline via standard W3C Web Streams. Completely eliminates child_process.fork() and inter-process serialization latency, delivering an 82% memory reduction (~22 MB flat idle footprint) and enabling native execution on edge isolates.<Link href="..."> and useRouter().push(...), while strictly typing route parameters with PageProps<T>, LayoutProps<T>, RouteParams<T>, and ErrorProps<T>.src/ map directly to URLs with nested layout.tsx files that wrap child pages without remounting during navigation, preserving client state, form inputs, audio playback, and scroll positions. Layouts feature independent data and cache lifecycles (layout_functions.ts).revalidate = false), strict seconds-based Incremental Static Regeneration (revalidate = N), and dynamic on-demand Server-Side Rendering (revalidate = 0).<DinouCacheSlot> to embed high-frequency reactive sections with micro-revalidation intervals (e.g., 5s) inside long-lived static shells, refreshable independently from the client (refreshSlot) or purged globally by tag (revalidateTag).<Suspense> holes concurrently stream over a single HTTP connection."use server" can be called directly from Client Components ("use client") wrapped inside <Suspense> boundaries. They can return rendered React components over Flight RPC, eliminating synthetic REST/GraphQL API boilerplate.bun build --compile), Deno (native single-file binaries deno compile), Cloudflare Workers (native workerd + Cloudflare KV for ISR), Netlify, or pure static exports.onRequest): Intercept requests and high-frequency webhooks (Stripe, Clerk, GitHub) at the HTTP layer in dinou.config.mjs returning native Web Response objects in <2ms, completely bypassing React component rendering.@swc/core) and WebSockets that patches updates into running applications while preserving active form inputs, counters, and component state across edits.npx dinou eject (or npm run eject) at any time to extract Dinou's complete compiler, server runtime, and React 19 flight pipeline directly into your project. Transparent, web standards-based, and free of proprietary black-box macros for seamless AI pair programming.public/)
Start building modern, full-stack React 19 applications in seconds. Choose between the automated CLI generator or manual configuration for full architectural control.
Before getting started, ensure your local environment meets the minimum requirements. Dinou is engineered to leverage modern JavaScript standards with zero transpile overhead:
module.register) for zero-fork server execution and Web Standard APIs (Request, Response, ReadableStream, Headers) natively supported in modern Node.js versions.npm run build:bun:compile.npm run build:deno:compile.npm run build:cloudflare and preview locally with wrangler dev.The fastest way to scaffold a ready-to-code Dinou application is using the official CLI starter. It sets up TypeScript, Tailwind CSS, sample Server Components, and the recommended directory layout:
# 1. Initialize a new Dinou application
npx create-dinou@latest my-app
# or
npm create dinou@latest my-app
# 2. Enter project directory
cd my-app
# 3. Launch development server (default: esbuild)
npm run dev
Open http://localhost:3000 in your browser to view your live application.
Prefer to wire up everything manually? Follow these step-by-step instructions to configure Dinou from scratch with complete transparency:
Initialize your project folder and install React 19, React DOM, and the dinou framework package:
# Create and enter directory
mkdir my-dinou-app
cd my-dinou-app
# Initialize package.json
npm init -y
# Install React 19 and Dinou
npm install react react-dom dinou
# Install TypeScript definitions for IDE intellisense
npm install -D @types/react @types/react-dom
Create the src directory with a root page (src/page.tsx) and the recommended root document layout (src/layout.tsx):
// src/page.tsx - React Server Component (RSC)
export default function Page() {
return (
<main className="p-8 max-w-4xl mx-auto space-y-4">
<h1 className="text-3xl font-bold tracking-tight">
Hello from Dinou!
</h1>
<p className="text-muted-foreground">
Rendered natively on the server with React 19 and zero client JavaScript overhead.
</p>
</main>
);
}
// src/layout.tsx - Root document layout
import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Dinou App</title>
</head>
<body>{children}</body>
</html>
);
}
The root layout.tsx defines the shared <html> and <body> document shell, metadata, and global providers so you don't have to repeat them on each page.
my-dinou-app/
├── public/ # Static root assets (served directly at /)
│ ├── favicon.ico
│ ├── robots.txt
│ └── site.webmanifest
├── src/
│ ├── page.tsx # Root page component (RSC)
│ ├── layout.tsx # Root document layout (<html/head/body>)
│ └── globals.css # Styles / Tailwind CSS
├── dinou.config.ts # Framework configuration (optional: ppr, plugins)
├── dinou-env.d.ts # Ambient framework typings
├── tsconfig.json # TypeScript configuration
├── package.json
└── .gitignore
Dinou does not force you to use TypeScript—you are completely free to write your application in standard JavaScript (.js / .jsx). However, even for pure JavaScript projects, defining a tsconfig.json in your project root is strongly recommended:
compilerOptions.paths from tsconfig.json to resolve clean path aliases like @/* into src/*."allowJs": true, code editors (VS Code, Cursor) use tsconfig.json to provide full autocomplete, auto-imports, and path resolution across all .js and .jsx files.{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
},
"allowJs": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": [
"src",
"dinou-env.d.ts"
]
}
Create dinou-env.d.ts in your root directory:
/// <reference types="dinou/env" />
The dinou/env reference automatically declares ambient typings for static assets (SVGs, images, web fonts) and CSS modules without manual type boilerplate.
Add the standard Dinou scripts to your package.json to control development, multi-runtime production builds (Node, Cloudflare, Deno, Bun), binary compilation, static exports, and ejecting:
{
"scripts": {
"dev": "dinou dev",
"build": "dinou build",
"start": "dinou start",
"dev:esbuild": "dinou dev:esbuild",
"dev:rollup": "dinou dev:rollup",
"dev:webpack": "dinou dev:webpack",
"build:node": "dinou build:node",
"start:node": "dinou start:node",
"build:cloudflare": "dinou build:cloudflare",
"build:deno": "dinou build:deno",
"build:deno:compile": "dinou build:deno:compile",
"start:deno": "dinou start:deno",
"build:bun": "dinou build:bun",
"build:bun:compile": "dinou build:bun:compile",
"start:bun": "dinou start:bun",
"export-static": "dinou export-static",
"eject": "dinou eject"
}
}
Note: For the complete catalog of targeted platform cross-compilation scripts (such as Windows, macOS, Linux ARM/x64), see the Complete CLI Scripts Catalog.
Create a .gitignore file to ignore dependencies, environment variables, build outputs, and Dinou caches:
node_modules
.dinou
out
dist
.wrangler
.env
.dinou/ holds internal dev artifacts and runtime builds.out/ contains pages generated by export-static.dist/ stores compiled single-executable binaries (Deno / Bun)..wrangler/ stores local state when testing Cloudflare Workers.Run the development server with the fast esbuild bundler (default), or choose Rollup or Webpack:
# Start with default ultra-fast esbuild
npm run dev
# Or start with Rollup or Webpack
npm run dev:rollup
npm run dev:webpack
Dinou provides both bundler freedom (esbuild, Rollup, Webpack) and universal runtime deployment (Node.js, Cloudflare Workers, Deno, Bun, or Static SSG) from a single codebase in src/ with zero code changes.
By default, Dinou uses esbuild for sub-second cold starts and in-memory compilation. When enterprise plugins or custom loaders are required, you can switch seamlessly to Rollup or Webpack with 100% feature parity:
# Start development with your preferred bundler
npm run dev # Default: esbuild
npm run dev:rollup # Dedicated Rollup in-memory engine
npm run dev:webpack # Dedicated Webpack engine
For advanced compiler tuning, custom plugins, and SWC options, visit the Bundlers Guide.
Compile your application for any execution environment without altering component logic:
npm run build:node && npm run start:node
npm run build:cloudflare && npx wrangler dev
npm run build:deno:compile # or build:bun:compile
Outputs standalone single-file executables directly to dist/.npm run export-static
Pre-rendered HTML and RSC Flight payloads output directly to out/.For complete deployment guides, Docker setups, and cloud hosting instructions, see the Deployment Guide.
Master Dinou's file-system router: nested layouts with independent Horizontal ISR, parallel slots with error isolation, route parameters, and layout control flags.
[!TIP] Looking for Type-Safe Routing? Dinou v7 introduces zero-runtime type safety with auto-generated route definitions, IDE autocomplete, and typed
PageProps&<Link>.
Every public route in Dinou maps directly to a folder inside src/ containing an entrypoint file (page.tsx, page.jsx, page.ts, or page.js). By default, pages are React Server Components (RSC) rendered natively on the server with zero client bundle overhead.
src/page.tsx → /src/blog/page.tsx → /blogsrc/blog/[slug]/page.tsx → /blog/hello-worldsrc/dashboard/settings/page.tsx → /dashboard/settingsYou never write central routing tables or configure URL maps manually. The folder structure inside src/ is your single source of truth.
When a request arrives (for example, /blog/hello-world), Dinou matches it entirely in memory:
src/blog/[slug]/page.tsx in memory with zero subprocess overhead or disk I/O latency.{ slug: "hello-world" }).src/layout.tsx → src/blog/layout.tsx).You can use .tsx, .ts, .jsx, or .js interchangeably across all pages and layouts.
Dinou provides expressive pattern matching for static, dynamic, optional, and catch-all URL segments:
| Pattern | Folder Structure | URL Match Example | Parsed params |
|---|---|---|---|
| Static | src/about/page.tsx | /about | {} |
| Dynamic | src/blog/[slug]/page.tsx | /blog/hello | { slug: "hello" } |
| Optional Single | src/shop/[[category]]/page.tsx | /shop/shop/shoes | { category: undefined }{ category: "shoes" } |
| Pattern | Folder Structure | URL Match Example | Parsed params |
|---|---|---|---|
| Catch-all (Required) | src/docs/[...slug]/page.tsx | /docs/routing/params | { slug: ["routing", "params"] } |
| Optional Catch-all | src/docs/[[...slug]]/page.tsx | /docs/docs/a/b | { slug: [] }{ slug: ["a", "b"] } |
Catch-all segments ([...slug]) consume all remaining path segments into a string array. Because they consume the remainder of the URL, Catch-all segments must always be the terminal (last) segment of a path definition.
Dinou enforces a strict No-Gap Rule when resolving nested optional segments such as /inventory/[[warehouse]]/[[aisle]]:
You can omit optional segments only if they are omitted from the tail end of the URL:
/inventory/main/a1 ✅ (both provided: { warehouse: "main", aisle: "a1" })/inventory/main ✅ (aisle omitted: { warehouse: "main", aisle: undefined })/inventory ✅ (both omitted: { warehouse: undefined, aisle: undefined })You cannot omit an earlier optional parameter while setting a later one:
/inventory//a1 ❌ (empty gap / skipped warehouse while setting aisle: ambiguous URL structure rejected)Dinou provides a clean, unified data contract. Route parameters are extracted from the URL and injected directly into components and server functions.
In Dinou v7, parameters are strictly typed using PageProps, LayoutProps, ErrorProps, and RouteParams:
page.tsx components: Page({ params }: PageProps<"/blog/[slug]">)layout.tsx components: Layout({ params, children }: LayoutProps<"/blog/[slug]">)error.tsx pages: Error({ error, params }: ErrorProps<"/blog/[slug]">)not_found.tsx 404 pages: NotFound({ params }: PageProps<"/blog/[slug]">)@slot/page.tsx): SlotPage({ params }: PageProps<"/dashboard/[teamId]">)getProps(params: RouteParams<"/blog/[slug]">) in page_functions.ts// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
interface BlogPostProps extends PageProps<"/blog/[slug]"> {
post?: { title: string; content: string };
}
export default function BlogPost({ params, post }: BlogPostProps) {
// ✅ params.slug is strongly typed as string automatically
return (
<article className="p-6">
<h1 className="text-2xl font-bold">{post?.title ?? params.slug}</h1>
<p className="mt-2 text-muted-foreground">{post?.content}</p>
</article>
);
}
[!IMPORTANT] Strict Signature Rule: In
page_functions.tsandlayout_functions.ts, Dinou passes the parameters dictionary directly as the first argument: usegetProps(params)andvalidateParams(params)(do not writegetProps({ params })). However, in React components (page.tsx,layout.tsx,error.tsx,not_found.tsx),paramsis passed as a component prop:export default function Page({ params }).
Exporting a validateParams(params) function from page_functions.ts or layout_functions.ts lets you enforce semantic validation rules before data fetching or rendering begins. Returning false (or a Promise that resolves to false) immediately aborts the request and issues a clean 404 response:
// src/product/[id]/page_functions.ts
import type { RouteParams } from "dinou";
export function validateParams(params: RouteParams<"/product/[id]">) {
// Ensure the product ID is strictly numeric; otherwise render 404
return /^\d+$/.test(params.id);
}
export async function getProps(params: RouteParams<"/product/[id]">) {
// Only called if validateParams returned true
const product = await db.products.findById(params.id);
return { product };
}
Guaranteed behavior across all rendering modes:
getProps, preventing wasteful database queries on invalid URLs.dinou export.Query parameters (e.g. ?search=react&sort=asc) are deliberately isolated from static route matching to preserve edge cache stability. In Dinou, useSearchParams() is a universal hook that works seamlessly in both Server Components and Client Components:
import { useSearchParams } from "dinou";
// Works identically in Server Components & "use client"!
export default function SearchPage() {
const searchParams = useSearchParams();
const q = searchParams.get("search") ?? "";
return <div>Search: {q}</div>;
}
Outside React components (where hooks cannot run), access the active query via getContext().req.query:
import { getContext } from "dinou";
// src/search/page_functions.ts
// getContext() is available in getProps, validateParams, etc.
export async function getProps() {
const { req } = getContext();
const search = req.query?.search;
return { search };
}
"use server"; // Declared at the top of the file
import { getContext } from "dinou";
// src/actions/items.ts
export async function filterItems() {
const { req } = getContext();
const search = req.query?.search;
return db.items.find({ q: search });
}
Dinou provides special files that cascade down the directory hierarchy. They allow you to compose nested layouts, manage independent ISR lifecycles, isolate errors, and break out of hierarchies using control flags.
A layout.tsx wraps all child pages and sub-layouts in its directory. On client navigation between sibling pages, parent layouts preserve their React state, input fields, scroll position, and DOM tree without re-mounting.
A layout component receives children, params, any parallel slots (e.g. sidebar), and props returned by layout_functions.ts:
// src/dashboard/[teamId]/layout.tsx
import type { ReactNode } from "react";
import type { LayoutProps } from "dinou";
interface DashboardLayoutProps extends LayoutProps<"/dashboard/[teamId]"> {
sidebar?: ReactNode; // Parallel slot (@sidebar)
}
export default function DashboardLayout({
children,
params,
sidebar,
}: DashboardLayoutProps) {
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r p-4">{sidebar}</aside>
<main className="flex-1 p-6">
<h1 className="text-xl font-bold">Team: {params.teamId}</h1>
{children}
</main>
</div>
);
}
In traditional SSR frameworks, layout shells and child pages are tightly coupled. Navigating to a dynamic child route often forces the entire layout to re-evaluate, resetting video playback, collapsing open menus, or wiping sidebar search state.
Dinou v7 introduces Horizontal Layered Segmentation. Layouts and pages travel on completely separate execution, streaming, and caching pathways:
layout_functions.ts<Page />) and streams into the layout's page slot.
page_functions.tsBecause layouts and pages are horizontally decoupled, they can declare completely independent caching lifetimes (TTL / ISR). A layout shell can cache for hours while its child pages revalidate every few seconds:
// src/dashboard/[teamId]/layout_functions.ts - Independent Layout ISR
import type { RouteParams } from "dinou";
// 1. Data Fetching for the Layout Shell
export async function getProps(params: RouteParams<"/dashboard/[teamId]">) {
const team = await fetchTeamData(params.teamId);
return { team };
}
// 2. Independent Cache Lifetime (1 hour)
// Child pages inside this dashboard can update every 5 seconds without invalidating this shell!
export const revalidate = 3600;
// 3. Granular Invalidation Tags (constant or function getCacheTags)
// Calling revalidateTag("team-shell") purges only the layout without wiping child page caches.
export const getCacheTags = ["team-shell"];
Props returned by getProps in layout_functions.ts are automatically injected into layout.tsx as top-level props alongside children, params, and any parallel slots.
Dinou allows you to break out of the nested layout hierarchy using empty flag files (files without extensions) placed in the route directory:
| Flag File | Applies To | Architectural Effect |
|---|---|---|
reset_layout | layout.tsx | Resets the layout tree. This layout becomes the new root layout, completely ignoring all parent layouts (including src/layout.tsx). Perfect for marketing landing pages or auth portals. |
no_layout | page.tsx, not_found.tsx, error.tsx | Bypasses all layouts entirely across the route. The page, custom 404, and error boundary render raw without any parent layout wrappers (ideal for embeddable widgets, checkout modals, or pure HTML canvas views). |
no_layout_error | error.tsx | Granular override: prevents parent layouts from wrapping the error boundary UI only, while preserving layouts for the regular page. |
no_layout_not_found | not_found.tsx | Granular override: prevents parent layouts from wrapping the custom 404 page only, while preserving layouts for the regular page. |
src/landing/layout.tsxsrc/landing/reset_layout (empty file)src/landing/page.tsxsrc/layout.tsx header and navigation bar.Create an error.tsx file in any route directory to catch and isolate runtime errors locally. If an exception occurs in a child component, Dinou bubbles up to the closest error.tsx, rendering it while keeping all parent layouts interactive:
// src/dashboard/[teamId]/error.tsx
import type { ErrorProps } from "dinou";
// Native: ErrorProps infers params and types the captured error
export default function DashboardError({
error,
params,
}: ErrorProps<"/dashboard/[teamId]">) {
return (
<div className="p-6 rounded-xl border border-rose-500/20 bg-rose-50/20">
<h3 className="text-rose-600 font-bold">Failed to load team {params.teamId}</h3>
<p className="text-xs text-muted-foreground mt-1">{error.message}</p>
{error.stack && (
<pre className="mt-3 p-3 bg-slate-900 text-slate-100 rounded text-xs overflow-auto">
{error.stack}
</pre>
)}
</div>
);
}
[!NOTE] Security Note: For production deployments,
error.stackis automatically stripped by Dinou to prevent internal file paths and system details from leaking. In development mode, the full cleaned stack trace is provided.
Define custom 404 views by placing not_found.tsx in any route directory. When an unmatched URL segment is requested, Dinou bubbles up to render the closest not_found.tsx template inside its enclosing layout.
// src/blog/[slug]/not_found.tsx (Scoped to /blog/[slug])
import { Link, type PageProps } from "dinou";
// Strongly types params.slug as string
export default function NotFound({ params }: PageProps<"/blog/[slug]">) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Article Not Found</h2>
<p className="mt-2 text-muted-foreground">
The article "{params.slug}" could not be located.
</p>
<Link
href="/"
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium text-sm"
>
Back to Home
</Link>
</div>
);
}
// src/not_found.tsx (Global Root Fallback)
import { Link, type PageProps } from "dinou";
// PageProps<"/"> strictly ensures no dynamic params are accidentally accessed
export default function GlobalNotFound({ params }: PageProps<"/">) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Page Not Found</h2>
<p className="mt-2 text-muted-foreground">
The requested page does not exist on this server.
</p>
<Link
href="/"
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg font-medium text-sm"
>
Back to Home
</Link>
</div>
);
}
Yes. If you define a page_functions.{ts,js} file in the same route directory as not_found.tsx, Dinou executes its getProps(params) server lifecycle and merges the returned data directly into the component's props alongside params:
// 1. Sibling server contract: src/blog/[slug]/page_functions.ts
export async function getProps(params: { slug: string }) {
const suggestions = await db.posts.findMany({ take: 3 });
return { suggestions };
}
// 2. 404 Page: src/blog/[slug]/not_found.tsx
import { Link, type PageProps } from "dinou";
interface NotFoundProps extends PageProps<"/blog/[slug]"> {
suggestions: { id: string; title: string; slug: string }[];
}
export default function NotFound({ params, suggestions }: NotFoundProps) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-3xl font-bold">404 - Article Not Found</h2>
<p className="mt-2 text-muted-foreground">
Could not find "{params.slug}". Here are some recommended articles:
</p>
{suggestions && (
<ul className="mt-4 space-y-1">
{suggestions.map((item) => (
<li key={item.id}>
<Link
href={`/blog/${item.slug}`}
className="text-primary hover:underline text-sm font-medium"
>
→ {item.title}
</Link>
</li>
))}
</ul>
)}
</div>
);
}
Difference with
error.tsx: Whilenot_found.tsxcan safely executegetProps,error.tsxpages never invoke server contracts (they receive strictlyErrorProps) to prevent compounding unhandled server exceptions.
Dinou provides dedicated directory conventions to organize enterprise codebases cleanly and compose parallel UI slots without impacting URL ergonomics:
Folders wrapped in parentheses (e.g. (auth), (marketing)) are omitted from the public URL path. They serve two fundamental architectural purposes in Dinou:
In conventional file-system routers, a single global layout forces all pages to share the same top navigation bar and footer. With Dinou Route Groups, you can create independent root layouts for marketing pages versus authentication screens:
src/
├── (marketing)/ # Public layout branch
│ ├── layout.tsx # Navbar, Footer, Hero
│ ├── page.tsx # → /
│ ├── about/page.tsx # → /about
│ └── pricing/page.tsx # → /pricing
└── (auth)/ # Minimalist auth branch
├── layout.tsx # Centered card, no Navbar
├── login/page.tsx # → /login
└── register/page.tsx # → /register
How Dinou Resolves Layouts:
src/(marketing)/layout.tsx and src/(auth)/layout.tsx live in distinct disk directories, so there is zero file collision./pricing: Dinou crawls into (marketing), matches pricing/page.tsx, and wraps it exclusively with the marketing layout./login: Dinou crawls into (auth), matches login/page.tsx, and wraps it exclusively with the auth layout.src/layout.tsx, both groups nest inside it (e.g. for HTML <head> or global fonts). If omitted, each group acts as a completely separate root layout.Because parenthesized folder names are stripped from the public URL, different route groups cannot define identical URL paths. For example, creating both src/(marketing)/about/page.tsx and src/(auth)/about/page.tsx would cause a build/resolution collision because both map to /about.
In Dinou, you do not need to prefix folders like components/, hooks/, or utils/ with an underscore. Any directory that simply does not contain a page.tsx (or page.jsx) is already completely ignored by the router and will never register a route.
The sole and specific purpose of prefixing a directory with an underscore (_folderName) is to escape the in-process route scanner when dealing with legacy code, drafts, or archived folders that actually contain page.tsx / page.jsx files inside them:
src/blog/
├── page.tsx → /blog (Active public route)
├── components/ (No page.tsx → already safe without "_")
│ └── post-card.tsx
└── _legacy_v1/ (Contains page.tsx, but "_" forces scanner to skip it!)
├── page.tsx (SKIPPED — NOT a route)
└── [slug]/page.tsx (SKIPPED — NOT a route)
Whenever Dinou's scanner encounters a folder starting with _, it completely bypasses that directory tree, ensuring that stale, archived, or legacy page files are never compiled into the production router.
Parallel routes allow you to render multiple independent pages simultaneously inside the same parent layout (e.g., @sidebar, @analytics, @modal):
Folder Structure:
src/dashboard/@sidebar/page.tsxsrc/dashboard/@analytics/page.tsxsrc/dashboard/page.tsx (main children)src/dashboard/layout.tsxLayout Prop Injection:
Slots are passed as named props directly alongside children:
import type { ReactNode } from "react";
import type { LayoutProps } from "dinou";
interface DashboardLayoutProps extends LayoutProps<"/dashboard"> {
sidebar?: ReactNode;
analytics?: ReactNode;
}
export default function Layout({
children,
sidebar,
analytics,
}: DashboardLayoutProps) {
return (
<div className="flex">
<aside>{sidebar}</aside>
<main>{children}</main>
<section>{analytics}</section>
</div>
);
}
Dinou isolates errors within parallel slots to prevent a single widget failure from crashing the entire page:
error.tsx: If the slot fails, only that specific slot renders its local error UI. The rest of the page and sibling slots remain completely operational.error.tsx: If an unhandled exception occurs in a slot with no error.tsx, Dinou safely falls back to rendering null for that slot without taking down the page.children page, and parent layouts remain fully mounted and interactive even if an individual slot fails to render.Master Dinou's client-side SPA navigation: smooth soft transitions, automatic hover prefetching, the Client Router Cache lifecycle, cache busting with the fresh prop, imperative useRouter controls, and Directory-First relative routing.
[!TIP] Looking for Type-Safe Navigation? Dinou generates automatic route types so
<Link href="...">androuter.push("...")provide zero-runtime autocomplete and compile-time error checking.
In Dinou, navigating between routes does not trigger a full browser refresh (window.location.reload). Instead, Dinou operates as a high-performance single-page application (SPA):
layout.tsx) never unmount, maintaining video playback, active form inputs, scroll positions, and local component state across page changes.startTransition, ensuring that the current UI stays fully responsive and interactive during data fetching.<Link> Component & PrefetchingThe primary way to navigate between pages is the <Link> component imported from "dinou". It intercepts click events, prevents hard reloads, and handles background prefetching:
import { Link } from "dinou";
export function NavigationBar() {
return (
<nav className="flex gap-4">
{/* ✅ Type-Safe href: autocompleted in your IDE via DinouRoute */}
<Link href="/dashboard">Dashboard</Link>
{/* ✅ Dynamic interpolation typed against registered routes */}
<Link href={`/blog/${post.slug}`}>Post</Link>
{/* Disable prefetch for heavy routes or rarely visited pages */}
<Link href="/analytics" prefetch={false}>
Analytics
</Link>
{/* ❌ TypeScript compile error on invalid/dead links when using type-safe routing */}
{/* <Link href="/broken-link">Broken</Link> */}
</nav>
);
}
[!NOTE] Native HTML
<a>Anchor Elements: In Dinou, standard HTML<a>tags also automatically trigger client-side soft navigation via global event delegation. However,<Link>is recommended because it provides compile-time type safety, background prefetching on hover, and thefreshprop.
To navigate imperatively from event handlers, timers, or form submissions, use the useRouter() hook inside Client Components ("use client"):
"use client";
import { useRouter } from "dinou";
export default function LoginButton() {
const router = useRouter();
const handleLogin = async () => {
await loginUser();
// Navigate programmatically
router.push("/dashboard");
// Or force fresh data after mutation:
// router.push("/dashboard", { fresh: true });
};
return <button onClick={handleLogin}>Log In</button>;
}
| Method | Behavior & Description |
|---|---|
router.push(href, options) | Navigates to the route and adds a new entry to the browser history stack. Supports { fresh: true }. |
router.replace(href, options) | Navigates to the route, replacing the current history entry without pushing a new history state. Supports { fresh: true }. |
router.back() | Navigates back to the previous page in history (equivalent to clicking the browser Back button). |
router.forward() | Navigates forward to the next page in the history stack. |
router.refresh() | Soft Reload: Re-fetches the current route data from the server and updates server components without losing client React state or scroll position. |
router.refreshSlot(slotId) | Re-fetches and updates an individual <DinouCacheSlot> component without re-rendering the surrounding page. |
To deliver instant page transitions without unnecessary server requests, Dinou features an in-memory Client Router Cache. Understanding how this cache operates is key to knowing when and why to use the fresh prop.
During client execution, Dinou maintains three dedicated in-memory caches in the browser tab:
pageCache: Stores prefetched and visited RSC promises keyed by pathname + search.scrollCache: Retains exact window and container scroll coordinates keyed by history state ID.popstate: Reads from pageCache and restores scroll without network requests on back/forward.<Link href="/pricing">, Dinou automatically prefetches the route payload in the background and stores the pending RSC promise directly into pageCache."/pricing". Because the payload was already prefetched into pageCache, the transition renders instantly with 0ms network latency.popstate events read from pageCache and restore the previous scroll position from scrollCache without making any network requests.While caching provides instantaneous navigation, pages with volatile data or pages visited immediately after a mutation (e.g., checkout, profile updates, or inventory counters) need fresh server state. The fresh prop provides explicit, predictable cache busting:
<Link fresh> does not prefetch potentially stale data before the user actually clicks.pageCache.delete(targetUrl), removing any prior cached RSC promise.// Ideal for live feeds, stock tickers, or carts
<Link href="/live-feed" fresh>
Live Feed
</Link>
// Navigate after a mutation to force fresh server data
const handleUpdate = async () => {
await updateProfile(data);
router.push("/profile", { fresh: true });
};
| Mechanism / Scope | Cache Behavior | Network Request | Typical Use Case |
|---|---|---|---|
Standard <Link> | Uses pageCache if prefetched or visited | 0ms if in cache; single request on first visit | General navigation, blogs, docs, marketing pages |
<Link fresh> / { fresh: true } | Purges pageCache.delete(url) | Always fetches live RSC payload from server | Post-mutation redirects, active carts, volatile feeds |
router.refresh() | Purges pageCache.delete(currentPath) | Re-fetches current route in-place | Polling, server function response handling, reload button |
| Hard Reload (F5) | Destroys all memory (all caches lost) | Full HTML document download + re-hydration | Emergency fallback; loses all client state |
In Dinou, all client-side navigations execute concurrently inside React 19's startTransition. This ensures that the current view remains fully interactive and visible while the target page's Server Components stream in from the server.
To inform users that a transition is occurring behind the scenes, Dinou provides the useNavigationLoading() hook:
true while a route payload is actively streaming or preparing in the background, and resets to false the moment React 19 commits the new page tree.false during server-side rendering (SSR), preventing hydration mismatches and flickering when the document first mounts.Mount a client loading indicator in your root layout to provide visual feedback (similar to GitHub or YouTube) whenever a route transition is pending:
"use client";
import { useNavigationLoading } from "dinou";
export function NavigationProgressBar() {
const isNavigating = useNavigationLoading();
if (!isNavigating) return null;
return (
<div
role="progressbar"
aria-label="Page loading"
className="fixed top-0 left-0 right-0 z-50 h-1 bg-blue-500/20 overflow-hidden pointer-events-none"
>
<div className="h-full bg-blue-600 dark:bg-blue-400 animate-pulse w-full transition-all duration-300" />
</div>
);
}
[!TIP] Because prefetched links resolve instantly in 0ms directly from
pageCache, this progress bar only surfaces on fresh fetches (such as navigation withfresh: trueor un-prefetched dynamic routes), keeping transitions ultra snappy.
Dinou resolves relative navigation paths (such as about, ./about, or ../dashboard) using a Directory-First Convention. This provides intuitive navigation that accurately mirrors your file system.
In a standard web browser, a URL like /blog/hello (without a trailing slash) treats hello as a file name, so a link to about resolves to /blog/about.
In Dinou, every route is treated as a directory because physically, each page is a folder containing a page.tsx component (e.g. src/blog/hello/page.tsx). Resolving relatively from a page navigates relative to that route's folder:
| Current Path | Relative Target | Resolved Path |
|---|---|---|
/blog/hello | about | /blog/hello/about |
/blog/hello | ./about | /blog/hello/about |
/blog/hello | ../about | /blog/about |
This rule applies uniformly across all navigation methods, including the <Link> component and programmatic calls like router.push().
Dinou v7 introduces a built-in, compile-time Type-Safe Routing engine with zero runtime overhead. During development and build time, Dinou automatically inspects your src/ directory and generates ambient TypeScript definitions in .dinou/types/routes.d.ts.
Unlike traditional routing setups that require external schema validators or proprietary bundler plugins, Dinou generates strict TypeScript definitions directly from your file system.
The system operates across two complementary architectural planes:
<Link href="...">, router.push("..."), or passing routes to reusable UI components with DinouRoute, TypeScript autocompletes valid routes, verifies dynamic template strings, and raises compile-time errors on dead links or typos.PageProps<T>, LayoutProps<T>, ErrorProps<T>, and RouteParams<T>.All route type validation occurs entirely within the TypeScript compiler. The emitted .dinou/types/routes.d.ts file contains ambient type declarations and is never bundled into your client JavaScript or server execution bundle:
Import <Link> or useRouter from "dinou". Your IDE will immediately suggest all valid static routes, parameterized dynamic routes, relative paths, queries, and hashes:
import { Link, useRouter } from "dinou";
export default function Navigation({ post }: { post: { slug: string } }) {
const router = useRouter();
return (
<nav className="flex gap-4">
{/* ✅ Instant autocompletion in your IDE */}
<Link href="/docs/routing">Routing Guide</Link>
{/* ✅ Dynamic routes with typed template strings */}
<Link href={`/blog/${post.slug}`}>View Post</Link>
{/* ✅ Seamless support for queries, hashes, relative paths, and external URLs */}
<Link href="/demo?tab=settings#section-1">Settings</Link>
<Link href="../contact">Relative</Link>
<Link href="https://github.com/roggc/dinou">GitHub</Link>
{/* ❌ TypeScript compile-time error when pointing to a non-existent route */}
{/* <Link href="/unknown-route" /> */}
<button onClick={() => router.push("/docs/api-reference")}>
View API Reference
</button>
</nav>
);
}
Type your components and data fetching functions by passing the route template string as a generic:
import type { PageProps, LayoutProps, RouteParams } from "dinou";
// 1. src/blog/[slug]/page.tsx
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed as string
return <h1>Post: {params.slug}</h1>;
// ❌ TypeScript error: Property 'id' does not exist on type '{ slug: string }'
// return <h1>{params.id}</h1>;
}
// 2. src/blog/[slug]/layout.tsx
export function BlogLayout({ children, params }: LayoutProps<"/blog/[slug]">) {
// ✅ params.slug is strongly typed; children is typed as ReactNode
return (
<section>
<header>Category: {params.slug}</header>
<main>{children}</main>
</section>
);
}
// 3. src/blog/[slug]/page_functions.ts
export async function getProps(params: RouteParams<"/blog/[slug]">) {
// ✅ Directly types the raw params object without params: any
return {
postTitle: `Post: ${params.slug}`,
};
}
| Exported Type | Primary Purpose | Canonical Example |
|---|---|---|
PageProps<T> | React Page components (page.tsx) | Page({ params }: PageProps<"/blog/[slug]">) |
LayoutProps<T> | React Layout components (layout.tsx) | Layout({ children, params }: LayoutProps<"/blog/[slug]">) |
ErrorProps<T> | React Error Page components (error.tsx) | Error({ error, params }: ErrorProps<"/blog/[slug]">) |
RouteParams<T> | Pure functions, data loaders & services (getProps, API helpers) | getProps(params: RouteParams<"/blog/[slug]">) |
DinouRoute | URL props, <Link>, router.push() & custom navigation components | interface NavProps { href: DinouRoute } |
Dinou deterministically transforms your file-system folder naming conventions into strict TypeScript definitions:
| Filesystem Convention | Inferred Parameter Type | Route Example | Type of params |
|---|---|---|---|
| Static Route | No parameters | src/about/page.tsx | Record<string, never> |
Single Dynamic [id] | string | src/blog/[id]/page.tsx | { id: string } |
Catch-All [...slug] | string[] | src/docs/[...slug]/page.tsx | { slug: string[] } |
Optional Dynamic [[lang]] | string | undefined | src/[[lang]]/page.tsx | { lang?: string } |
Optional Catch-All [[...opt]] | string[] | undefined | src/shop/[[...opt]]/page.tsx | { opt?: string[] } |
| Multiple Dynamic Segments | Combined by segment | src/users/[uId]/posts/[pId]/page.tsx | { uId: string; pId: string } |
| Nested Optionals | Positional dependent | src/inv/[[wh]]/[[aisle]]/page.tsx | { wh?: string; aisle?: string } |
[!WARNING] Be cautious with trailing slashes in dynamic links!
In TypeScript, template literal types evaluate
${string}as matching any string, including the empty string""(because"" extends stringevaluates totrue).The scenario: Consider a nested route such as
src/posts/[id]/[slug]/page.tsx, whose generated template literal type is`/posts/${string | number}/${string | number}`.If a developer accidentally leaves off the second parameter but appends a trailing slash:
<Link href={`/posts/${id}/`}>View</Link>TypeScript matches the first parameter with
id, the slash with literal/, and the second parameter with empty string"". However, at runtime Dinou normalizes trailing slashes, requesting/posts/123(which results in a 404 Not Found because nopage.tsxexists at the parent segment).Best Practice: Always avoid trailing slashes when building dynamic URLs. Written without the trailing slash (
`/posts/${id}`), TypeScript accurately flags the missing parameter at compile-time.
When typing a component, type PageProps<""> and press Ctrl + Space (or Cmd + Space on macOS). Your editor will list every registered route pattern in the project:
import type { PageProps } from "dinou";
// Trigger Ctrl + Space inside "" to pick from project routes
export default function BlogPost({ params }: PageProps<"/blog/[slug]">) {
return <h1>{params.slug}</h1>;
}
Error boundaries in Dinou v7 strongly type both the captured error and the current route's parameters:
"use client";
import { useRouter, type ErrorProps } from "dinou";
export default function BlogPostError({ params, error }: ErrorProps<"/blog/[slug]">) {
const router = useRouter();
return (
<div className="error-card p-6 border rounded-xl bg-destructive/10">
<h2>Failed to load post: {params.slug}</h2>
<p className="text-sm text-muted-foreground">{error.message}</p>
{/* Soft reload via Dinou client router */}
<button onClick={() => router.refresh()}>Try Again</button>
</div>
);
}
When creating design-system components (e.g., custom buttons, sidebar items, dropdown menus), use the exported DinouRoute union type to pass through autocomplete and validation:
import type { DinouRoute } from "dinou";
import { Link } from "dinou";
interface NavButtonProps {
href: DinouRoute; // 👈 Inherits full autocompletion and route validation
label: string;
}
export function NavButton({ href, label }: NavButtonProps) {
return (
<Link href={href} className="px-3 py-1.5 rounded-lg bg-blue-600 text-white font-medium">
{label}
</Link>
);
}
// In your application code:
const menu: { label: string; href: DinouRoute }[] = [
{ label: "Overview", href: "/docs" }, // ✅ Autocompleted
{ label: "Routing", href: "/docs/routing" }, // ✅ Autocompleted
// { label: "Broken", href: "/non-existent-page" }, // ❌ TypeScript compile error!
];
Dinou types template literal strings against dynamic route definitions, catching broken template strings while accepting valid parameters:
import { Link, useRouter } from "dinou";
interface Product {
id: string;
name: string;
}
export function ProductCard({ product }: { product: Product }) {
const router = useRouter();
return (
<div className="card p-4 border rounded-xl">
{/* String interpolation verified against `/products/${string | number}` */}
<Link href={`/products/${product.id}`}>{product.name}</Link>
<button onClick={() => router.push(`/products/${product.id}`)}>
View Details
</button>
</div>
);
}
Dinou coordinates compilation, route scanning, and TypeScript declarations through a zero-dependency architecture:
src/ Directory: Synchronous read and structural crawl of all page and layout files.generate-route-types): Low-overhead in-memory scanner completes in under 2ms..dinou/types/routes.d.ts): Emits ambient module declarations extending the "dinou" package interface with exact union types (DinouRoute) and parameter maps (DinouRouteParams).tsconfig.json without requiring editor restarts or language server reboots.How Dinou keeps route typings strictly synchronized across local development and deployment pipelines:
npm run dev, Dinou arms a low-overhead filesystem watcher on src/. Whenever a page.(tsx|jsx|ts|js) file is added, renamed, or deleted, .dinou/types/routes.d.ts regenerates in under 2ms.npm run build runs route type generation as step 0 before bundling. If code references a non-existent route, TypeScript type checking fails the build before deploying.npx tsc --noEmit in CI pipelines to validate link integrity across the entire project deterministically.export default function Page({ params }: any) or generic PageProps during progressive migrations..dinou/types/routes.d.ts has not yet been generated (e.g., fresh clone prior to npm run dev or npm run build), types gracefully fallback to permissive strings (string & {}), ensuring clean git checkout and initial install steps without blocking developer workflows.Dinou is built natively on the React 19 Server Components (RSC) specification. Rather than treating your entire application as a single monolithic client-side bundle, Dinou partitions components into two distinct execution environments:
"use client". Also pre-rendered to HTML on the server (SSR) for instant first paint, and then hydrated with interactive JavaScript in the browser to enable local state (useState, useActionState), event listeners (onClick), and browser APIs.Dinou's dual-component model allows you to seamlessly mix server-rendered backbones with interactive client islands:
| Feature | Server Components (RSC) | Client Components |
|---|---|---|
| Default in Dinou? | Yes (all components inside src/) | No (requires explicit "use client") |
| Execution Environment | Server only (Node.js, Bun, Deno, Cloudflare Workers) | Server (pre-rendering SSR) + Browser (hydration) |
| Client JS Bundle Impact | 0 KB (Zero client JavaScript overhead) | Ships component JS code to the browser |
| Backend & Secret Access | Direct access to DB, ORM, filesystem, private env vars | Cannot access backend resources or private secrets |
| Interactivity & State | Pure static output; no state or DOM event listeners | Full React state, hooks, effects, and DOM events |
| Data Fetching Pattern | Direct top-level await inside component body | Props passed from RSC, React 19 use(), or Server Functions |
Think of Server Components as the backbone of your application that orchestrates data fetching, layouts, and non-interactive UI (rendering pure HTML with zero client JavaScript). Client Components act as interactive "islands" or leaves embedded inside the server tree.
The server streams HTML and React Flight JSON representations across an open HTTP connection. The browser consumes this stream without ever downloading the JavaScript source code of your Server Components.
In Dinou, every component inside src/ is a Server Component by default unless explicitly marked with "use client". They execute exclusively on the server and never ship their implementation code to the browser.
async functions. You can await database queries, ORM calls, microservices, or files directly inside the component body.<Suspense>: When slow asynchronous operations are wrapped in React <Suspense>, the layout shell and fallback UI can be delivered immediately without waiting for slow queries, while data streams progressively into the HTTP response.// src/users/page.tsx (Server Component by default)
import { getContext } from "dinou";
import { db } from "@/lib/db"; // Never bundled to the client!
export default async function UsersPage() {
// Direct backend query without an intermediary REST API
const users = await db.user.findMany({ take: 10 });
return (
<main className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold">Registered Users</h1>
<ul className="mt-4 divide-y">
{users.map((user) => (
<li key={user.id} className="py-2 flex justify-between">
<span>{user.name}</span>
<span className="text-xs font-mono text-muted-foreground">{user.email}</span>
</li>
))}
</ul>
</main>
);
}
[!NOTE] TTFB & Execution Timing Note:
- Static by Default (SSG): In Dinou, this page is pre-rendered at build time by default. The database query runs during
npm run build, serving static HTML to visitors with instant TTFB (0ms server wait).- Dynamic Routes: If the route is marked dynamic (e.g.
revalidate = 0inpage_functions.tsor reading cookies), a top-levelawaitblocks the entire HTTP response until the query finishes. To preserve instant TTFB on dynamic routes, delegate theawaitto a child component wrapped in React<Suspense>.
<Suspense>By delegating asynchronous queries to child components wrapped in <Suspense>, the outer layout and fallback UI are sent immediately:
// src/users/page.tsx - Non-blocking streaming layout
import { Suspense } from "react";
import { db } from "@/lib/db";
import { UsersSkeleton } from "./skeleton";
export default function UsersPage() {
return (
<main className="p-8 max-w-4xl mx-auto">
{/* 1. The outer shell is sent immediately (instant TTFB) */}
<h1 className="text-2xl font-bold">Registered Users</h1>
{/* 2. Displays fallback while the child query resolves */}
<Suspense fallback={<UsersSkeleton />}>
<UsersList />
</Suspense>
</main>
);
}
// Child component handles the async query independently
async function UsersList() {
const users = await db.user.findMany({ take: 10 });
return (
<ul className="mt-4 divide-y">
{users.map((u) => (
<li key={u.id} className="py-2 flex justify-between">
<span>{u.name}</span>
<span className="text-xs font-mono text-muted-foreground">{u.email}</span>
</li>
))}
</ul>
);
}
<Suspense> Behaves Across Rendering Strategies| Strategy | Compiler & Runtime Behavior | Fallback Skeleton Behavior | Trade-off |
|---|---|---|---|
| Pure SSG (Static) | The compiler automatically awaits all Suspense boundaries during npm run build and writes the resolved HTML directly into the static artifact. | Visitors never see the loading skeleton because the fallback is eliminated once the build settles. | 0ms TTFB from CDN edge, but data reflects build-time state (stale until next build/revalidation). |
| Pure Dynamic SSR | The server flushes the layout shell and <Suspense fallback={<Skeleton />}> immediately over the open HTTP connection. | The user sees the skeleton while waiting, and React streams the resolved HTML chunk when the query finishes. | 100% fresh data and non-blocking layout, but incurs continuous server compute costs, DB load, and request-time latency. |
| Partial Prerendering (PPR) | The outer layout shell and the fallback skeleton are pre-rendered at build time and served immediately with 0ms TTFB from the CDN edge. | The skeleton renders instantly from static cache, while the server concurrently executes only the dynamic query and streams live data into the hole. | Combines the instant delivery of SSG with the fresh, live data of dynamic SSR at minimal server compute cost. |
Because Server Components do not execute in the browser, they cannot use browser-specific or client-interactive features:
useState, useEffect, useReducer, useLayoutEffect.onClick, onChange, onSubmit, onKeyDown.window, document, localStorage, navigator, geolocation.useContext() or use(Context). (While Server Components can render <Context> providers for client children, they cannot read context values themselves).Client Components provide rich, real-time client-side interactivity. They enable event handlers, local component state, animations, and access to browser APIs.
Client Components have complete access to the client-side React ecosystem:
useState, useReducer, and React 19 useActionState.useEffect and useLayoutEffect.localStorage, Geolocation, IntersectionObserver, Audio).A common point of confusion is assuming Client Components execute exclusively in the browser. In Dinou, Client Components are pre-rendered on the server to HTML during initial page load to ensure instant First Contentful Paint (FCP) and optimal SEO indexing before hydrating in the browser.
[!WARNING] SSR Execution Safety Rule: Because Client Components run on the server during pre-rendering, accessing browser globals directly in the root of the component function (e.g.
const token = localStorage.getItem("token")) will throw a server runtime error.Always defer browser-only API access inside
useEffector guard it with an environment check:if (typeof window !== "undefined") { const token = localStorage.getItem("auth_token"); }
"use client" DirectiveTo declare a component as a Client Component, add the "use client" directive at the very top of the file, before any import statements or comments.
The directive is not a runtime function call—it is an instruction to the bundler (esbuild, Rollup, Webpack) that defines the network split point:
"use client"; // Must be at the very top of the file!
import { useState } from "react";
export function Counter({ initial = 0 }: { initial?: number }) {
const [count, setCount] = useState(initial);
return (
<button
onClick={() => setCount((c) => c + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium"
>
Count: {count}
</button>
);
}
Once a file declares "use client", all components, utility modules, and third-party libraries imported by that file are automatically treated as part of the client bundle. You do not need to add "use client" to child modules imported by a Client Component.
When passing props from a Server Component to a Client Component, data crosses the network boundary (serialized as React Flight JSON). Props passed across this boundary must be serializable.
string, number, boolean, null, undefined, bigint.{ id: 1, name: "Dinou" }).Date objects are serialized and reconstructed by React on the client.children or named slot props.use())."use server".onClick={() => ...}) from Server Components.window, document.body).use()React 19 supports passing an unresolved Promise as a prop from a Server Component to a Client Component. The Client Component reads the promise synchronously using the use() hook, automatically triggering Suspense fallbacks until the promise settles:
// Server Component
import { Suspense } from "react";
import { UserProfile } from "./user-profile"; // "use client"
export default function Page() {
// Start fetching without awaiting!
const userPromise = fetchUserData();
return (
<Suspense fallback={<div>Loading profile skeleton...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Client Component (user-profile.tsx)
"use client";
import { use } from "react";
export function UserProfile({ userPromise }: { userPromise: Promise<{ name: string; role: string }> }) {
// Unwraps the promise value seamlessly inside Suspense without useEffect
const user = use(userPromise);
return <h2>Hello, {user.name} ({user.role})</h2>;
}
Dinou empowers you to choose the exact architectural balance between server performance and client ergonomics. Follow these battle-tested patterns for optimal architecture:
Keep your layouts, route pages, and static wrappers as Server Components. Only push the "use client" boundary down to small, interactive "leaf" components (such as an upvote button, search box, or dropdown menu).
src/dashboard/layout.tsx → Server Component (zero client JS)
src/dashboard/page.tsx → Server Component (fetches data directly)
src/dashboard/_components/search-bar.tsx → "use client" (interactive leaf)
src/dashboard/_components/action-button.tsx → "use client" (interactive leaf)
A Client Component cannot directly import a Server Component (because bundling a Server Component into the client module would defeat the RSC boundary). However, a Client Component can receive Server Components as children or via named JSX props!
// Client Component: ModalContainer.tsx
"use client";
import { useState, type ReactNode } from "react";
export function ModalContainer({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<div className="modal-backdrop">
{/* children is rendered on the server and passed as pre-rendered JSX! */}
{children}
</div>
)}
</div>
);
}
// Server Component: page.tsx
import { ModalContainer } from "./ModalContainer";
import { HeavyServerProfile } from "./HeavyServerProfile"; // Server Component
export default function Page() {
return (
<ModalContainer>
{/* HeavyServerProfile remains 100% on the server! */}
<HeavyServerProfile />
</ModalContainer>
);
}
getProps)If you are building an interactive canvas, video editor, or complex SPA dashboard and prefer traditional React where you can use useState and onClick anywhere in the page without boundary splitting, use Dinou's Client-First Model:
// src/canvas/page_functions.ts - Runs securely on the server
import { db } from "@/lib/db";
export async function getProps(params: { id: string }) {
const project = await db.projects.findById(params.id);
return { project };
}
// src/canvas/page.tsx - Entire page is a Client Component
"use client";
import { useState } from "react";
export default function CanvasPage({ project }: { project: any }) {
// Full access to state, effects, and events everywhere in the file!
const [layers, setLayers] = useState(project.initialLayers);
return (
<div onClick={() => console.log("clicked")}>
<Toolbar onAddLayer={(l) => setLayers([...layers, l])} />
<Stage layers={layers} />
</div>
);
}
[!TIP] Advantage: Fast development speed and traditional React ergonomics, while preserving secure backend data fetching and SSR pre-rendering.
<DinouCacheSlot> (Vertical Micro-ISR)A common architectural trap in dynamic RSC applications is converting heavy server widgets into Client Components just to gain client-side memory caching (such as SWR or TanStack Query). In Dinou, <DinouCacheSlot> provides component-level server caching (Vertical Segmentation) directly in the RSC layer.
"use client" with a client cache. This bloats the client JS bundle, loses direct database access, exposes internal API endpoints, and introduces client-side network waterfalls.<DinouCacheSlot>): Keep the component on the server! <DinouCacheSlot> caches the rendered RSC Flight payload of that specific subtree on the server for a designated TTL (e.g. 300s). The rest of the page remains 100% dynamic, while the expensive subtree serves in 0ms.<DinouCacheSlot> Contain Client Components?Yes, absolutely. While <DinouCacheSlot> itself is an async Server Component that executes exclusively on the server, its children can freely render interactive Client Components. Dinou caches the Client Component reference along with its pre-computed props. The client hydrates it normally with full interactivity (events, state, animations):
// 1. Client Component: interactive-chart.tsx (Hydrates with 100% interactivity)
"use client";
import { useState } from "react";
export function InteractiveChart({ data }: { data: any }) {
const [zoom, setZoom] = useState(1);
return (
<div className="p-4 border rounded-lg bg-card">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-bold">Metrics (Zoom: {zoom}x)</span>
<button
onClick={() => setZoom((z) => z + 1)}
className="px-2 py-1 text-xs bg-primary text-primary-foreground rounded"
>
Zoom In
</button>
</div>
<canvas data-zoom={zoom} />
</div>
);
}
// 2. Server Component: page.tsx (Dynamic page with an isolated cached slot)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { UserGreeting } from "./UserGreeting"; // Reads user session/cookies
import { GreetingSkeleton, AnalyticsSkeleton } from "./skeletons";
import { InteractiveChart } from "./interactive-chart"; // Client Component
// Server Component performing the heavy query
async function HeavyAnalyticsWidget() {
const analyticsData = await db.analytics.aggregateBigData(); // 2-second query
return <InteractiveChart data={analyticsData} />;
}
export default function DashboardPage() {
return (
<main className="space-y-6 p-8">
{/* Dynamic personal component: reads cookies/session without blocking initial layout delivery (preserves fast TTFB) */}
<Suspense fallback={<GreetingSkeleton />}>
<UserGreeting />
</Suspense>
{/* Micro-ISR hole: wrapped in Suspense so cache misses stream without blocking TTFB */}
<Suspense fallback={<AnalyticsSkeleton />}>
<DinouCacheSlot id="analytics-slot" duration={300} tags={["analytics"]}>
<HeavyAnalyticsWidget />
</DinouCacheSlot>
</Suspense>
</main>
);
}
<Suspense>? While <DinouCacheSlot> works without it on cache hits, on a cache miss (cold start or expired TTL) an unwrapped slot would block the entire HTTP response during the heavy query. Wrapping it in <Suspense> guarantees instant TTFB by streaming the layout and skeleton immediately.UserGreeting? In dynamic SSR, wrapping UserGreeting in <Suspense> ensures that reading user session or auth cookies does not delay the initial response, keeping TTFB fast. In PPR, it also prevents dynamic request data from de-optimizing the build-time static shell.refreshSlot("analytics-slot") from "dinou" without reloading the page or resetting state in other components.revalidateTag("analytics").<DinouCacheSlot>What happens when you nest <DinouCacheSlot> inside a React <Suspense> boundary on a page with Partial Prerendering (PPR)? You unlock the highest performance profile possible in full-stack web engineering: zero-delay initial delivery paired with near-zero streaming delay and 99.9% database query offloading.
| Metric | PPR Alone (Without Slot) | PPR + <DinouCacheSlot> |
|---|---|---|
| Architecture | Static shell + on-demand dynamic execution | Instant shell + cached streaming hole |
| TTFB (First Byte) | 0ms from CDN edge (instant outer layout) | 0ms from CDN edge (instant outer layout) |
| Skeleton Duration | The user waits the full query duration (e.g. 1,200ms) on every single request while the server executes the query. | ~5ms! Subsequent visitors receive the cached RSC stream chunk almost immediately; the skeleton barely blinks. |
| Database Load | 10,000 visitors per minute = 10,000 database queries executed on the server. | 10,000 visitors per minute with duration={60} = 1 database query per minute (99.99% reduction). |
Wrap the slow asynchronous Server Component with <DinouCacheSlot>, and then enclose both inside React <Suspense>:
// src/dashboard/page.tsx - High-traffic operational dashboard
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { MetricsSkeleton } from "./MetricsSkeleton";
import { InteractiveMetricsChart } from "./InteractiveMetricsChart"; // "use client"
// Heavy async Server Component
async function HeavyMetricsSubtree() {
// Heavy aggregation across millions of database rows (e.g. 1.5 seconds)
const aggregatedStats = await db.telemetry.aggregateOperations();
return <InteractiveMetricsChart data={aggregatedStats} />;
}
export default function DashboardPage() {
return (
<main className="p-8 space-y-6 max-w-5xl mx-auto">
{/* 1. PPR Static Shell: Delivered instantly with 0ms TTFB from edge CDN */}
<header className="border-b pb-4">
<h1 className="text-3xl font-extrabold tracking-tight">Operations Command Center</h1>
<p className="text-sm text-muted-foreground">Real-time system telemetry and infrastructure health.</p>
</header>
{/* 2. PPR Dynamic Boundary + Cache Slot:
- Fallback skeleton sent immediately in initial layout
- Dynamic hole streams from server cache in ~5ms for subsequent users
- Database query executes only ONCE every 60 seconds */}
<Suspense fallback={<MetricsSkeleton />}>
<DinouCacheSlot id="operations-telemetry" duration={60} tags={["telemetry"]}>
<HeavyMetricsSubtree />
</DinouCacheSlot>
</Suspense>
</main>
);
}
0ms from CDN edge.revalidateTag("telemetry") or refreshSlot() to update without full-page reloads.<DinouCacheSlot>?A common question in full-stack architecture is: Does <DinouCacheSlot> only make sense for dynamic components or subtrees?
The short answer: While dynamic routes and PPR are where it delivers exponential returns, it also unlocks unique capabilities for static pages (such as asymmetric revalidation and in-place client slot refreshes). Understanding when to use it boils down to the distinction between Horizontal Segmentation (route/layout-level) and Vertical Segmentation (component-level).
<DinouCacheSlot> (Redundant or Anti-Patterns)page.tsx body inside a slot reinvents horizontal caching. Use export const revalidate = 300 in page_functions.ts instead.<DinouCacheSlot> Delivers 10x Value (Recommended Scenarios)<DinouCacheSlot> isolates the expensive widget with its own TTL:// src/routes/dashboard/page.tsx (Dynamic authenticated SSR)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { getSession } from "@/lib/auth";
import { GlobalMetricsWidget } from "./metrics";
export default async function DashboardPage() {
const session = await getSession(); // Evaluated dynamically per user
return (
<div className="p-6">
<h1>Welcome back, {session.user.name}</h1> {/* 100% Live & personal */}
{/* Global widget: cached for 10 minutes on the server across all users */}
<Suspense fallback={<div>Loading global telemetry...</div>}>
<DinouCacheSlot id="global-metrics" duration={600} tags={["metrics"]}>
<GlobalMetricsWidget />
</DinouCacheSlot>
</Suspense>
</div>
);
}
Dynamic Holes in Partial Prerendering (PPR) — The Ultimate Synergy: In PPR, the static shell ships instantly from CDN (0ms TTFB), while dynamic holes stream in via React Suspense. Without a slot, 100,000 visitors trigger 100,000 database hits. Placing <DinouCacheSlot> inside the hole means only the first request queries the DB; the other 99,999 stream the cached RSC Flight payload in ~2-5ms.
Static or ISR Pages with Asymmetric Lifecycles & Targeted Refreshes: This is where <DinouCacheSlot> proves it is not limited to dynamic pages. You can use it in static/ISR pages for two critical architectural advantages:
revalidate = 86400). But live stock pricing fluctuates every 60 seconds. A slot lets the pricing widget expire every 60s without re-rendering the whole page!refreshSlot): A Client Component can trigger an on-demand re-render of only this Server Component using refreshSlot(slotId) without refreshing the browser page or losing client state (scroll position, inputs, audio):// 1. In your Page (Server Component): Asymmetric slot inside static route
<DinouCacheSlot id={`price-${product.id}`} duration={60} tags={["pricing"]}>
<LivePriceCalculator productId={product.id} />
</DinouCacheSlot>
// 2. In an interactive Client Component: refresh ONLY that slot
"use client";
import { refreshSlot } from "dinou";
export function RefreshPriceButton({ productId }: { productId: string }) {
return (
<button onClick={() => refreshSlot(`price-${productId}`, { fresh: true })}>
Check Latest Stock & Price
</button>
);
}
| Question / Requirement | Recommended Action | Reason |
|---|---|---|
| Dynamic or PPR route with heavy queries? | Wrap with <DinouCacheSlot> inside <Suspense> | Protects database from repetitive execution and streams cached RSC Flight chunks in ~2-5ms. |
| Asymmetric expiration within a page? | Use <DinouCacheSlot> | Decouples widget TTL (e.g. 60s) from overall page TTL (e.g. 24h) without re-rendering the entire page. |
| Need surgical, targeted client refreshes? | Use <DinouCacheSlot> + refreshSlot() | Updates just that server subtree in-place without page reload or state loss. |
| Pure SSG with uniform expiration? | Do NOT use <DinouCacheSlot> | Let build-time SSG and CDN Edge caching handle the entire page with zero runtime overhead. |
"use client" at the root layout. Marking src/layout.tsx as a Client Component cascades the client boundary to every single page in your application. Root layouts should always be Server Components to protect secrets and enable streaming.import ServerComponent from "./ServerComponent" inside a file containing "use client". Use the Composition Pattern (Section 6.2) and pass it as children instead.window.innerWidth or localStorage.getItem() outside of a useEffect in a Client Component causes server pre-rendering crashes. Always wrap browser calls in useEffect or an environment check (if (typeof window !== "undefined"))."use server") for actions.useEffect on mount. In modern RSC architectures, fetching on mount via useEffect creates network waterfalls, causes layout shifts, and misses server streaming. Pass data from Server Components or use Strategy 9 (RSC Promise) / Strategy 12 (Server Function RPC).In Dinou, every route segment can independently configure its data fetching, static pre-rendering, cache lifecycles, and parameter validation without cluttering component UI code.
page_functions.ts: Placed alongside page.tsx. Configures data fetching (getProps), static paths (getStaticPaths), cache strategy (revalidate), Partial Prerendering (ppr), cache tags (getCacheTags), and parameter validation (validateParams) specifically for that page.layout_functions.ts: Placed alongside layout.tsx. Supplies data directly to the layout component (getProps) and manages segment-level cache strategy (revalidate, ppr) independently from child pages.Dinou strictly separates server-side data orchestration and cache configuration from presentation UI:
src/blog/
├── layout.tsx # Presentation shell (RSC)
├── layout_functions.ts # Layout cache strategy (revalidate = 300) & getProps
└── [slug]/
├── page.tsx # Page presentation (RSC or "use client")
└── page_functions.ts # Page data fetching (getProps), paths, and validation
RouteSegmentConfig<TParams, TProps>In Dinou, every server-side configuration file implements the exact same foundational contract: RouteSegmentConfig. This is the framework's underlying "abstract" interface defining all segment capabilities. For day-to-day application code, Dinou exports two concrete, role-based aliases: PageFunctions (for pages) and LayoutFunctions (for layouts):
// Exported directly from "dinou":
/**
* Foundational "abstract" interface governing all route segments in Dinou.
*/
export interface RouteSegmentConfig<TParams = any, TProps = any> {
// Partial Prerendering (PPR) configuration
ppr?: boolean | (() => boolean | Promise<boolean>);
// Cache & revalidation strategy (< 1 = Dynamic SSR, >= 1 = ISR, false = Static)
revalidate?: number | false | (() => number | false | Promise<number | false>);
// Cache tags for on-demand invalidation via revalidateTag(tag)
getCacheTags?: string[] | ((params?: TParams) => string[] | Promise<string[]>);
// Path generator for SSG (constant array or sync/async function)
getStaticPaths?: StaticPathItem<TParams>[] | (() => StaticPathsResult<TParams>);
// Server loader providing direct props to the component
getProps?: (params?: TParams) => TProps | Promise<TProps>;
// Parameter validator guard (returns false to halt and return 404)
validateParams?: (params?: TParams) => boolean | Promise<boolean>;
// Controls whether Incremental Static Generation (ISG) is allowed for ungenerated paths
allowISG?: boolean | (() => boolean | Promise<boolean>);
}
/**
* Concrete semantic alias to import and use in 'page_functions.ts'
*/
export type PageFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;
/**
* Concrete semantic alias to import and use in 'layout_functions.ts'
*/
export type LayoutFunctions<TParams = any, TProps = any> = RouteSegmentConfig<TParams, TProps>;
| Where to use | Type to import | Purpose |
|---|---|---|
src/.../page_functions.ts | PageFunctions | Types data loading (getProps), static paths, and cache for the page. |
src/.../layout_functions.ts | LayoutFunctions | Types data loading (getProps) and independent cache for the layout shell. |
| Plugins & Shared Utils | RouteSegmentConfig | Base "abstract" interface for generic utilities that process arbitrary route segments. |
import type { PageFunctions } in a page file immediately clarifies intent and self-documents that the functions belong to the page tier.Use getProps to load data on the server based on route params and pass it directly to your component.
[!NOTE] v7 Direct Return Contract: In Dinou v7,
getPropsdirectly returns the props object ({ ... }). The legacy wrapper{ page: { ... }, layout: { ... } }has been deprecated because pages and layouts now have their own dedicated configuration files.
params?: TParams (The normalized dynamic route parameters)TProps | Promise<TProps> (Injected directly as the component's props)// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";
export interface BlogPostData {
post: Post;
}
// Best Practice: Type getProps using PageFunctions for strict parameters & return contract
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
const post = await db.getPost(params?.slug);
// Directly returns BlogPostData for page.tsx
return { post };
};
// src/blog/[slug]/page.tsx
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";
// Best Practice: Extend PageProps with the loader's returned BlogPostData
interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}
export default function BlogPost({ params, post }: BlogPostProps) {
return (
<article>
<span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// src/blog/layout_functions.ts
import type { LayoutFunctions } from "dinou";
import { db } from "@/lib/db";
export interface BlogLayoutData {
categories: string[];
}
// Best Practice: Type layout server functions with LayoutFunctions
export const getProps: LayoutFunctions<any, BlogLayoutData>["getProps"] = async () => {
const categories = await db.getCategories();
// Directly returns props for layout.tsx
return { categories };
};
// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";
// Best Practice: Extend LayoutProps for automatic typing of children and params
interface BlogLayoutProps extends LayoutProps, BlogLayoutData {}
export default function BlogLayout({
children,
categories,
}: BlogLayoutProps) {
return (
<div className="blog-shell">
<aside className="blog-sidebar">
<h3>Categories</h3>
<ul>
{categories.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
</aside>
<main>{children}</main>
</div>
);
}
getProps Only Receives params & The Dynamic Bailout MechanismRoute params (e.g. /blog/[slug]) identify the resource, not the visitor. Because the content for a given route is identical for everyone, Dinou can run getProps in advance—at build time (SSG) or on first request (ISR)—and safely pre-render and cache the HTML (in static storage, server memory, reverse proxies, or CDNs).
In contrast, cookies, headers, and query params belong to a specific HTTP connection. If getProps relied on them, the output could contain private user data, making shared caching impossible and forcing the server to render every request on demand.
If you call getContext() inside getProps to read request-time connection data:
| Property Accessed | Behavior | Why? |
|---|---|---|
context.request.headers | ❌ Build Error (SSG) / Dynamic Bailout (SSR) | Headers vary per client request. Accessing them invalidates shared static caching. |
context.request.cookies | ❌ Build Error (SSG) / Dynamic Bailout (SSR) | Cookies are user-specific (sessions, auth). Cannot be baked into shared HTML. |
context.searchParams | ❌ Build Error (SSG) / Dynamic Bailout (SSR) | Query strings vary infinitely (?ref=..., ?utm_source=...). Cannot be pre-rendered at build time. |
context.params | ✅ Allowed Everywhere (SSG, ISR, SSR, PPR) | Identifies the route resource ([slug]). Pure and deterministically cacheable. |
To keep pages pre-renderable (SSG/ISR) and instant to load, organize your data into its natural layer: route data in getProps, session cookies inside <Suspense>, and query parameters via the universal useSearchParams() hook:
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";
export interface BlogPostData {
post: Post;
}
// 1. Pure route loader: depends solely on params
// Cached statically for all visitors (SSG / ISR)
export const getProps: PageFunctions<RouteParams<"/blog/[slug]">, BlogPostData>["getProps"] = async (params) => {
const post = await db.getPost(params?.slug);
return { post };
};
// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPostData } from "./page_functions";
import { UserSession } from "./user-session";
import { RelatedPosts } from "./related-posts";
import { SearchFilter } from "./search-filter";
interface BlogPostProps extends PageProps<"/blog/[slug]">, BlogPostData {}
export default function BlogPostPage({ params, post }: BlogPostProps) {
return (
<article className="post-container">
{/* 1. Instant static shell: delivered immediately from cache */}
<header>
<span className="text-xs text-muted-foreground">Route: /blog/{params.slug}</span>
<h1>{post.title}</h1>
{/* Client Component using useSearchParams() for instant input sync */}
<SearchFilter />
</header>
<div className="post-body">{post.content}</div>
{/* 2. Dynamic cookie component streamed via Suspense */}
<Suspense fallback={<div className="text-xs text-muted-foreground">Loading session...</div>}>
<UserSession />
</Suspense>
{/* 3. Server Component using useSearchParams() streamed via Suspense */}
<Suspense fallback={<div className="text-xs text-muted-foreground">Filtering articles...</div>}>
<RelatedPosts />
</Suspense>
</article>
);
}
// src/blog/[slug]/user-session.tsx (Server Component)
import { getContext } from "dinou";
import { auth } from "@/lib/auth";
// Cookie & auth access isolated inside Suspense:
// Streaming this component prevents getProps from bailing out to Dynamic SSR!
export async function UserSession() {
const { req } = getContext();
const sessionToken = req?.cookies?.session;
const user = sessionToken ? await auth.verify(sessionToken) : null;
if (!user) {
return <a href="/login" className="text-xs font-semibold">Sign In</a>;
}
return (
<aside className="user-badge">
<span>Welcome back, <strong>{user.name}</strong>!</span>
</aside>
);
}
// src/blog/[slug]/related-posts.tsx (Server Component)
import { useSearchParams } from "dinou";
import { db } from "@/lib/db";
// Universal hook in a Server Component:
// Reads ctx.req.query to fetch filtered data from the DB on the server.
// Because it reads query, wrap it in <Suspense> in page.tsx so the post shell stays static!
export async function RelatedPosts() {
const searchParams = useSearchParams();
const tag = searchParams.get("tag") ?? "featured";
const articles = await db.getPostsByTag(tag);
return (
<section className="related-posts">
<h3>Articles matching "?tag={tag}"</h3>
<ul>
{articles.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</section>
);
}
// src/blog/[slug]/search-filter.tsx (Client Component)
"use client";
import { useSearchParams } from "dinou";
// Universal hook in a Client Component:
// Reads URL params on the browser for reactive input state without contacting the server
export function SearchFilter() {
const searchParams = useSearchParams();
const currentTag = searchParams.get("tag") ?? "";
return (
<input
type="search"
defaultValue={currentTag}
placeholder="Filter by tag (?tag=...)..."
className="text-xs border rounded px-2 py-1"
/>
);
}
| Data Layer | Handled In | Lifecycle & Strategy |
|---|---|---|
| 1. Route Data (Params) | getProps in page_functions.ts | Pure and cacheable for all visitors (SSG / ISR). |
| 2. Cookies & Headers | Server Components wrapped in <Suspense> | Streams dynamically without bailing out the static page shell. |
| 3. Universal useSearchParams() | Server or Client Components with <Suspense> | Works in Server Components (streamed) and Client Components (instant UI). |
Defines which dynamic parameter combinations should be pre-rendered at build time (SSG). By default, any dynamic path not returned is generated on-demand when first requested and cached statically for all subsequent visitors (Incremental Static Generation / ISG), unless disabled via allowISG: false (which strictly restricts routes to the pre-rendered list and returns 404 for any other path).
In Dinou v7, getStaticPaths supports both a constant array export (for known, fixed paths) and a synchronous or asynchronous function export (for dynamic data sources like databases or headless CMSs):
// 1. Constant Array Export (Zero runtime overhead, ideal for fixed routes or locales)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = [
"welcome",
"getting-started",
"release-notes",
];
// 2. Async Function Export (Queries databases or headless CMSs at build time)
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[slug]">]["getStaticPaths"] = async () => {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map((p) => p.slug);
};
Dinou accommodates simple, catch-all, and multi-segment architectures with flexible item formats:
| Route Type | Accepted Format | Example Return | Pre-rendered URLs |
|---|---|---|---|
Simple ([id] or [slug]) | string[] | number[] | ["foo", "bar"] or [1, 2, 3] |
Optional Simple ([[id]]) | `(string | number | undefined)[]` |
Catch-all ([...slug]) | string[][] | [["a", "b"], ["c"]] | /docs/a/b, /docs/c |
Optional Catch-all ([[...slug]]) | `(string[] | undefined)[]` | [undefined, ["a"], ["a", "b"]] |
Multi-Segment / Nested ([cat]/[id]) | `Record<string, string | string[]>[]` | [{ cat: "shoes", id: "100" }] |
Simple dynamic routes match a single URL segment. Dinou supports both required segments ([id]) and optional segments ([[category]]):
// src/blog/[id]/page_functions.ts (Required segment [id])
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/blog/[id]">]["getStaticPaths"] = [
"1",
"2",
"3",
]; // Generates: /blog/1, /blog/2, /blog/3
// src/shop/[[category]]/page_functions.ts (Optional segment [[category]])
import type { PageFunctions, RouteParams } from "dinou";
// Pass undefined to pre-render the root route (/shop) without a parameter
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[[category]]">]["getStaticPaths"] = [
undefined, // Generates: /shop
"shoes", // Generates: /shop/shoes
"shirts", // Generates: /shop/shirts
];
Catch-all routes match multiple dynamic path segments. Dinou supports both standard catch-alls ([...slug]) and optional catch-alls ([[...slug]]):
// src/docs/[...slug]/page_functions.ts (Required catch-all [...slug])
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[...slug]">]["getStaticPaths"] = [
["intro"], // Generates: /docs/intro
["api", "v1", "auth"], // Generates: /docs/api/v1/auth
];
// src/docs/[[...slug]]/page_functions.ts (Optional catch-all [[...slug]])
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[[...slug]]">]["getStaticPaths"] = [
undefined, // Generates: /docs (root)
["intro"], // Generates: /docs/intro
["api", "v1", "auth"], // Generates: /docs/api/v1/auth
];
Static parameters automatically propagate downwards through the directory tree. If you define values for a segment, Dinou will recursively generate all nested static sub-pages under those values.
src/blog/[slug]/
├── page.tsx # Page: /blog/[slug]
├── page_functions.ts # Returns ["post-a", "post-b"]
└── details/
└── page.tsx # Nested static page
If getStaticPaths in blog/[slug] returns ["post-a", "post-b"], Dinou generates 4 static pages:
/blog/post-a/blog/post-a/details/blog/post-b/blog/post-b/detailsWhen nesting routes where both the parent and the child segment have their own page.tsx, static generation follows a strict top-down "chain of responsibility":
src/shop/[category]/
├── page.tsx # Parent page: /shop/[category]
├── page_functions.ts # Parent gatekeeper
└── [id]/
├── page.tsx # Child page: /shop/[category]/[id]
└── page_functions.ts # Child generator
Because the parent folder contains a page.tsx, Dinou must generate valid parent pages first before it can traverse down to the child level. The parent acts as a mandatory gatekeeper:
[]: Dinou halts at the parent level and never invokes the child [id]/page_functions.ts. No product pages are generated.// 1. Parent: src/shop/[category]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]">]["getStaticPaths"] = [
"shoes",
"shirts",
];
// 2. Child: src/shop/[category]/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[id]">]["getStaticPaths"] = [
"100",
"200",
];
// Automatically generates 4 static pages:
// /shop/shoes/100, /shop/shoes/200
// /shop/shirts/100, /shop/shirts/200
[!TIP] Rule of Thumb:
- If an intermediate folder has its own
page.tsx, define apage_functions.tsat that folder level with scalar arrays (string[]).- If an intermediate folder does not have a
page.tsx, you can define all nested parameters together in the leafpage_functions.tsusing an Object Array.
When you have multiple dynamic segments in a path without intermediate pages (the intermediate folders do not have a page.tsx), the leaf segment returns an Object mapping all parameter names at once:
src/shop/[category]/[brand]/[...specs]/
└── page.tsx # Only leaf has a page.tsx
// src/shop/[category]/[brand]/[...specs]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
export const getStaticPaths: PageFunctions<RouteParams<"/shop/[category]/[brand]/[...specs]">]["getStaticPaths"] = [
{
category: "electronics",
brand: "apple",
specs: ["m3", "16gb"], // → Pre-renders: /shop/electronics/apple/m3/16gb
},
{
category: "clothing",
brand: "nike",
specs: ["running", "shoes", "size-10"], // → Pre-renders: /shop/clothing/nike/running/shoes/size-10
},
];
getStaticPaths can be an async function, allowing you to query external headless CMSs, databases, or REST APIs during the build:
// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
export const getStaticPaths: PageFunctions<RouteParams<"/products/[id]">]["getStaticPaths"] = async () => {
// Queries your database at build time (npm run build)
const products = await db.products.findMany({ select: { id: true } });
return products.map((product) => String(product.id));
};
Validates dynamic parameters (such as [id] or [...slug]) on the server prior to rendering. If validateParams returns false (or a falsy value), Dinou immediately terminates request processing and issues a 404 Not Found response.
params?: TParams (The normalized route parameters object)boolean | Promise<boolean> (Return false to abort and render 404)// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
type ProductParams = RouteParams<"/products/[id]">;
// 1. Synchronous validation: guarantees id is purely numeric
export const validateParams: PageFunctions<ProductParams>["validateParams"] = (params) => {
return /^[0-9]+$/.test(params?.id ?? "");
};
// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
type BlogParams = RouteParams<"/blog/[slug]">;
// 2. Asynchronous validation: verifies record existence before rendering
export const validateParams: PageFunctions<BlogParams>["validateParams"] = async (params) => {
if (!params?.slug) return false;
const exists = await db.posts.exists({ slug: params.slug });
return exists;
};
[!NOTE] Execution Timing:
validateParamsexecutes beforegetProps. If validation fails,getPropswill never run, preventing unnecessary database lookups.
Controls whether dynamic paths that were not pre-rendered at build time (via getStaticPaths during dinou build) are allowed to be generated on-demand on the first incoming user request.
In Dinou v7, pre-rendering happens strictly during the build phase, ensuring production server startup is instantaneous.
true): Any valid path not pre-generated at build time is generated on-demand when the first visitor requests it, and subsequently cached statically (Incremental Static Generation / ISG).false): Strictly restricts accessible routes to those explicitly returned by getStaticPaths. Any request for a path not pre-rendered immediately returns a 404 Not Found without invoking getProps or running SSR.// src/docs/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
// Strict SSG: only these two articles exist; all other /docs/[slug] return 404
export const getStaticPaths: PageFunctions<RouteParams<"/docs/[slug]">]["getStaticPaths"] = [
"installation",
"architecture",
];
// Disallows on-demand generation for unlisted slugs
export const allowISG: PageFunctions["allowISG"] = false;
The revalidate export is Dinou's single source of truth for route caching and rendering behavior. It is measured in seconds (s).
| Revalidate Value | Strategy | Lifecycle & Behavior |
|---|---|---|
revalidate < 1 (or 0) | Dynamic SSR | Escapes static generation. The page is rendered dynamically on every incoming request with live database reads. |
revalidate >= 1 | Incremental Static Regeneration (ISR) | The page is cached statically for N seconds. Requests within the window receive instant cached HTML (0ms TTFB). After N seconds, a background regeneration is triggered. |
revalidate = false | Pure SSG (Default) | The page is rendered once at build time and cached indefinitely until the next build or manual purge. |
// src/metrics/page_functions.ts - Real-time dynamic SSR
import type { PageFunctions } from "dinou";
// Escapes SSG: executes getProps on every request
export const revalidate: PageFunctions["revalidate"] = 0;
// src/store/[id]/page_functions.ts - 60-second ISR
import type { PageFunctions, RouteParams } from "dinou";
export const revalidate: PageFunctions["revalidate"] = 60; // 1 minute ISR
// Conditional revalidation based on environment
export const revalidate: PageFunctions["revalidate"] = () => {
return process.env.NODE_ENV === "development" ? 0 : 3600;
};
Assigns custom tags to the static page cache files, allowing the page to be purged and regenerated on-demand using revalidateTag(tag) (On-Demand Revalidation).
// src/products/[id]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
type ProductParams = RouteParams<"/products/[id]">;
// 1. Constant Array
export const getCacheTags: PageFunctions["getCacheTags"] = ["catalog", "products"];
// 2. Synchronous Function (derives tags from route parameters)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = (params) => {
return ["catalog", `product-${params?.id}`];
};
// 3. Asynchronous Function (queries external tags or taxonomy)
export const getCacheTags: PageFunctions<ProductParams>["getCacheTags"] = async (params) => {
const categories = await db.categories.forProduct(params?.id);
return ["catalog", ...categories.map((c) => `category-${c.slug}`)];
};
When data changes, call revalidateTag() in a Server Function or webhook to instantly invalidate all cached files tagged with that identifier:
// src/actions/products.ts
"use server";
import { revalidateTag } from "dinou";
import { db } from "@/lib/db";
export async function updateProduct(id: string, data: any) {
await db.products.update({ where: { id }, data });
// Instantly invalidates /products/[id] and any catalog pages sharing the tag
await revalidateTag(`product-${id}`);
}
Partial Prerendering (PPR) combines the instant TTFB (Time to First Byte) of a static page with the real-time capabilities of dynamic server-side rendering over a single HTTP response.
PPR eliminates the traditional trade-off between static and dynamic rendering by dividing the page into two phases over a single HTTP connection:
0ms TTFB) from CDN edge or server cache. Includes layouts, navigation, and fallback skeletons.<Suspense> execute concurrently on the server and stream their resolved HTML chunks directly into the page.// src/dashboard/page_functions.ts
import type { PageFunctions } from "dinou";
// Enables Partial Prerendering for this route segment
export const ppr: PageFunctions["ppr"] = true;
// src/dashboard/page.tsx
import { Suspense } from "react";
import { UserGreeting, UserGreetingSkeleton } from "./user-greeting";
import { LiveMetrics, MetricsSkeleton } from "./live-metrics";
export default function DashboardPage() {
return (
<div className="dashboard-layout">
{/* Static Shell: Pre-rendered at build time (0ms TTFB) */}
<header className="dashboard-header">
<h1>Management Console</h1>
</header>
{/* Dynamic Holes: Streamed progressively over the open HTTP connection */}
<Suspense fallback={<UserGreetingSkeleton />}>
<UserGreeting />
</Suspense>
<Suspense fallback={<MetricsSkeleton />}>
<LiveMetrics />
</Suspense>
</div>
);
}
To achieve complete type safety, autocompletion, and compile-time validation in your routes, import PageFunctions and LayoutFunctions from "dinou" and pass your parameter and props types:
src/blog/[slug]/page_functions.ts// src/blog/[slug]/page_functions.ts
import type { PageFunctions, RouteParams } from "dinou";
import { db, type Post } from "@/lib/db";
// 1. Strict route parameters derived from URL pattern
export type BlogParams = RouteParams<"/blog/[slug]">;
// 2. Define data contract passed to the component
export interface BlogPageProps {
post: Post;
views: number;
}
// 3. Page configuration with full type inference
export const ppr: PageFunctions["ppr"] = true;
export const revalidate: PageFunctions["revalidate"] = 60; // 1 min ISR
export const getStaticPaths: PageFunctions<BlogParams>["getStaticPaths"] = [
"welcome",
"announcement",
];
export const getProps: PageFunctions<BlogParams, BlogPageProps>["getProps"] = async (params) => {
const post = await db.posts.findUnique({ where: { slug: params?.slug } });
if (!post) throw new Error("Post not found");
return { post, views: 100 };
};
export const validateParams: PageFunctions<BlogParams>["validateParams"] = (params) => {
return Boolean(params?.slug && params.slug.length > 2);
};
export const getCacheTags: PageFunctions<BlogParams>["getCacheTags"] = (params) => {
return ["blog", `post-${params?.slug}`];
};
src/blog/[slug]/page.tsx// src/blog/[slug]/page.tsx
import { Suspense } from "react";
import type { PageProps } from "dinou";
import type { BlogPageProps } from "./page_functions";
import { RealtimeViews, ViewsSkeleton } from "./components";
// Extend PageProps to receive both route params and loader data with full type safety
interface BlogPostPageProps extends PageProps<"/blog/[slug]">, BlogPageProps {}
export default function BlogPostPage({ params, post }: BlogPostPageProps) {
return (
<article className="max-w-2xl mx-auto py-8">
{/* 1. Static shell (pre-rendered at build time, delivered with 0ms TTFB) */}
<span className="text-xs text-muted-foreground font-mono">/blog/{params.slug}</span>
<h1 className="text-3xl font-bold">{post.title}</h1>
<div className="text-sm text-muted-foreground my-2">
By {post.author} • published {post.publishedAt}
</div>
<div className="prose mt-6">{post.content}</div>
{/* 2. PPR Dynamic Stream: Real-time analytics streamed in via Suspense */}
<Suspense fallback={<ViewsSkeleton />}>
<RealtimeViews slug={params.slug} />
</Suspense>
</article>
);
}
src/blog/layout_functions.ts// src/blog/layout_functions.ts
import type { LayoutFunctions, RouteParams } from "dinou";
import { db } from "@/lib/db";
// 1. Layout parameters for /blog (static route segment without dynamic params = {})
type BlogLayoutParams = RouteParams<"/blog">;
// 2. Define layout data contract
export interface BlogCategory {
id: string;
name: string;
count: number;
}
export interface BlogLayoutData {
categories: BlogCategory[];
featuredSlug: string;
}
// 3. Dedicated layout loader typed via LayoutFunctions
export const getProps: LayoutFunctions<BlogLayoutParams, BlogLayoutData>["getProps"] = async () => {
const categories = await db.categories.findMany();
const featured = await db.posts.findFirst({ where: { featured: true } });
// Injected directly as props into layout.tsx
return {
categories,
featuredSlug: featured?.slug ?? "welcome",
};
};
// 4. Segment-level caching strategy for the layout shell
export const revalidate: LayoutFunctions["revalidate"] = 300; // Layout revalidates every 5 mins
export const ppr: LayoutFunctions["ppr"] = true;
src/blog/layout.tsx// src/blog/layout.tsx
import type { LayoutProps } from "dinou";
import type { BlogLayoutData } from "./layout_functions";
// Extend LayoutProps to receive children, segment params, and layout loader data
interface BlogLayoutComponentProps extends LayoutProps<"/blog">, BlogLayoutData {}
export default function BlogLayout({
children,
categories,
featuredSlug,
}: BlogLayoutComponentProps) {
return (
<div className="blog-container">
<aside className="blog-sidebar">
<h3>Categories</h3>
<ul>
{categories.map((c) => (
<li key={c.id}>
{c.name} ({c.count})
</li>
))}
</ul>
</aside>
<main className="blog-content">
{children}
</main>
</div>
);
}
If you prefer defining all configurations in a single typed object before exporting, use object destructuring:
// 1. Page Config Object in src/blog/[slug]/page_functions.ts
const pageConfig: PageFunctions<BlogParams, BlogPageProps> = {
ppr: true,
revalidate: 60,
getStaticPaths: ["welcome", "announcement"],
getProps: async (params) => {
const post = await fetchPost(params?.slug);
return { post, views: 100 };
},
validateParams: (params) => Boolean(params?.slug),
getCacheTags: (params) => ["blog", `post-${params?.slug}`],
};
export const { ppr, revalidate, getStaticPaths, getProps, validateParams, getCacheTags } = pageConfig;
// 2. Layout Config Object in src/blog/layout_functions.ts
const layoutConfig: LayoutFunctions<any, BlogLayoutData> = {
revalidate: 300,
ppr: true,
getProps: async () => ({
categories: await fetchCategories(),
featuredSlug: "welcome",
}),
};
export const { revalidate, ppr, getProps } = layoutConfig;
Dinou is Static by Default. It provides a continuum of rendering strategies—from build-time SSG and stale-while-revalidate ISR to real-time Dynamic SSR, revolutionary Partial Prerendering (PPR), and component-level Micro-ISR with <DinouCacheSlot>.
In Dinou, every route is assumed to be static until proven otherwise. During dinou build, the compiler analyzes route segments, components, and loader functions to evaluate whether a route can be pre-rendered or requires real-time request data:
revalidate: 0, Dinou automatically bails out to Live SSR on every incoming request.<DinouCacheSlot>.Pure static rendering pre-generates HTML and RSC files during the build phase (npm run build). Production server startup is instantaneous, and static pages achieve the fastest possible Time to First Byte (TTFB) cached by Edge CDNs globally.
Any page that does not consume request-specific headers, cookies, or query parameters and does not set revalidate < 1 is automatically generated as static HTML:
// src/about/page.tsx
// Automatically rendered as static HTML during build time ('npm run build')
export default function AboutPage() {
return (
<article className="prose">
<h1>About Us</h1>
<p>This page is pre-rendered once and served statically forever.</p>
</article>
);
}
If a Server Component reads request-specific data at render time without wrapping it inside a dynamic streaming boundary (React <Suspense> with Partial Prerendering / PPR enabled), Dinou automatically "bails out" of static generation and transitions the entire page into Dynamic SSR for that request:
// src/profile/page.tsx
import { getContext } from "dinou";
import db from "@/lib/db";
export default async function ProfilePage() {
const context = getContext();
// ⚠️ Reading cookies outside <Suspense> triggers an Automatic Static Bailout
// This page will now be rendered dynamically on every incoming request
const sessionToken = context?.req.cookies["session_token"];
const user = await db.users.findByToken(sessionToken);
return <h1>Welcome back, {user?.name}</h1>;
}
Automatic bailout occurs when reading context.req.cookies, context.req.headers, or context.req.query (URL search parameters) during server component execution without wrapping the access in a React <Suspense> boundary.
useSearchParams()?Client Components ("use client") that call useSearchParams() do not trigger dynamic bailout. The page remains 100% static (SSG). During build time, the component is pre-rendered with an empty parameter set, and live URL parameters are synchronized in the browser upon hydration.
[!TIP] Preventing Hydration Mismatches with Search Parameters:
If a Client Component immediately renderssearchParams.get("q")in its initial JSX markup, the server-generated static HTML (where the query was empty at build time) will differ from the live browser DOM (e.g.?q=react), triggering a React hydration mismatch error.Recommended Fix: Keep initial render deterministic, then read and apply live search parameters inside
useEffect()after mounting:// src/components/search-filter.tsx "use client"; import { useSearchParams } from "dinou"; import { useState, useEffect } from "react"; export function SearchFilter() { const searchParams = useSearchParams(); const [query, setQuery] = useState<string | null>(null); useEffect(() => { // Synchronize client-side only after mounting -> eliminates hydration mismatch setQuery(searchParams.get("q") ?? ""); }, [searchParams]); if (query === null) { return <span className="text-muted-foreground">Loading filter...</span>; } return <span>Filter: <strong>{query}</strong></span>; }Alternative (Live SSR): If the initial HTML sent over the wire must contain search parameters for SEO, switch the page to Live SSR by exporting
export const revalidate = 0;inpage_functions.ts.
revalidate Strategy (Strict Seconds)In Dinou v7, the revalidate export in page_functions.ts (or layout_functions.ts) serves as the single source of truth for route rendering behavior. It is measured strictly in seconds (s):
Value of revalidate | Strategy | Description |
|---|---|---|
false (or omitted) | Pure Static (SSG) | Pre-rendered once at build time. Cached indefinitely until manual invalidation. |
0 (or number < 1) | Dynamic SSR | Rendered on every incoming HTTP request on the fly. |
N (number >= 1) | Time-Based ISR | Pre-rendered, served instantly from cache, and revalidated in the background every N seconds. |
revalidate < 1 or 0)To force a page to render dynamically on every single request (escaping the default static generation), set revalidate to 0:
// src/analytics/live/page_functions.ts
// Escapes static generation: executes dynamic SSR on every request
export const revalidate = 0;
[!NOTE] Migration from Legacy
dynamicExport: In older versions of Dinou, dynamic rendering was toggled viaexport function dynamic(). In Dinou v7, thedynamicexport has been completely removed. Settingexport const revalidate = 0;is the official unified mechanism to escape static generation.
revalidate >= 1)When revalidate >= 1, Dinou serves pre-rendered static HTML/RSC payloads while regenerating the page in the background at most once every N seconds (stale-while-revalidate):
// src/blog/page_functions.ts
// Rebuilds the cached page in the background at most once every 60 seconds
export const revalidate = 60; // Strict seconds (1 minute)
DenoKVStorage, CloudflareKVStorage, MemoryStorage) without needing a persistent disk.Partial Prerendering (PPR) resolves the decades-old dilemma between static and dynamic rendering. Instead of forcing an entire page to be 100% static or 100% dynamic, PPR delivers a pre-rendered static shell instantly (0ms TTFB), while streaming dynamic holes in parallel over the same HTTP response.
At build time, Dinou compiles the layout, navigation, and static components into an immutable HTML shell. Any dynamic component wrapped in React <Suspense> is saved as a streaming placeholder hole (<div data-ppr-hole="slot-id">...fallback...</div>).
<Suspense> on the server.useEffect() + fetch() anti-patterns, suffering waterfalls, layout shifts, and spinners.0ms TTFB from the edge, while the component inside <Suspense> executes on the server on every incoming request. You get 100% fresh, live database data streamed directly to the browser without client fetch waterfalls.Without PPR, dynamic data access is infectious: if a single nested component reads request cookies or headers, Dinou triggers an Automatic Static Bailout for the entire page, destroying static pre-rendering and edge caching for the whole route.
With PPR enabled (ppr = true), React <Suspense> acts as an isolated sandbox. Dynamic data consumption inside the boundary does not escape to parent or sibling components:
// src/store/page.tsx
import { Suspense } from "react";
import { getContext } from "dinou";
import db from "@/lib/db";
import { ProductGallery } from "@/components/gallery";
import { CartBadgeSkeleton } from "@/components/skeletons";
// ⚠️ Dynamic component: reads request-specific session cookie
async function LiveCartBadge() {
const context = getContext();
const sessionToken = context?.req.cookies["session_id"];
const cart = await db.carts.findActive(sessionToken);
return <div className="cart-badge">Items: {cart?.itemCount ?? 0}</div>;
}
export default function StorePage() {
return (
<main className="container">
{/* 1. Static Shell: 0ms TTFB, pre-rendered during build */}
<header className="navbar">
<h1>Flagship Store</h1>
{/* 2. PPR Boundary: Isolates cookie bailout exclusively to this hole */}
<Suspense fallback={<CartBadgeSkeleton />}>
<LiveCartBadge />
</Suspense>
</header>
{/* 3. Static Sibling: Remains 100% static and edge-cached */}
<ProductGallery />
</main>
);
}
cookies["session_id"] turns the entire page into dynamic SSR. Edge caching is lost, and the visitor waits with a blank screen.<Suspense>. The navbar and product gallery are delivered instantly at 0ms TTFB. Only the cart badge executes on the server and streams into place.Dinou provides granular control over Partial Prerendering across three hierarchical levels:
page_functions.ts) — Highest Priority: A page can override any parent layout or global configuration:
// src/shop/products/[slug]/page_functions.ts
export const ppr = true; // Guarantees PPR on product details
layout_functions.ts) — Subtree Cascading: Applies to that layout and cascades to all its descendant subroutes:
// src/admin/layout_functions.ts
export const ppr = false; // Disables PPR for /admin and ALL subroutes
dinou.config.mjs): Enables PPR globally across all eligible routes:
// dinou.config.mjs
import { defineConfig } from "dinou";
export default defineConfig({ ppr: true });
<DinouCacheSlot>)Traditional full-stack frameworks only offer Horizontal Segmentation (caching an entire URL route or layout shell). When a dynamic page contains an expensive database widget, developers are often forced into an architectural trap: converting the widget to a Client Component with client-side cache libraries (SWR, TanStack Query), introducing bundle bloat, waterfall requests, and layout shifts.
Dinou v7 introduces Vertical Segmentation with <DinouCacheSlot> from "dinou/server". It keeps components securely on the server as React Server Components (RSC) while caching their rendered serialized Flight payload in server storage with dedicated TTLs and invalidation tags.
<DinouCacheSlot> functions as a universal caching primitive across two core architectural patterns:
The route runs in Live SSR mode (evaluating user cookies and personal greetings live). The heavy analytics calculation is cached on the server for 300 seconds and wrapped in React <Suspense>:
// src/dashboard/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { FastUserProfile, SlowAnalyticsWidget } from "./widgets";
import { AnalyticsSkeleton } from "./skeletons";
export default function DashboardPage() {
return (
<main className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
{/* 1. Dynamic Component: Evaluated live on every request (reads session/cookies) */}
<FastUserProfile />
{/* 2. Micro-ISR Slot: Cached on the server for 300 seconds */}
{/* ⚠️ Wrapped in <Suspense> so cache misses stream without blocking TTFB */}
<Suspense fallback={<AnalyticsSkeleton />}>
<DinouCacheSlot id="analytics-widget" tag="analytics" revalidate={300}>
<SlowAnalyticsWidget />
</DinouCacheSlot>
</Suspense>
</main>
);
}
The parent route has a slow horizontal revalidation of 24 hours (revalidate: 86400). Inside it, a volatile inventory counter revalidates vertically every 60 seconds (revalidate={60}) or on-demand:
// src/products/[slug]/page_functions.ts
export const revalidate = 86400; // Slow horizontal revalidation (24h)
// src/products/[slug]/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { ProductHero, ProductDescription } from "./static-widgets";
import { LiveStockCounter, StockSkeleton } from "./stock-widgets";
export default function ProductDetailPage({ params }: { params: { slug: string } }) {
return (
<main className="container py-8 space-y-6">
{/* 1. Heavy Static Content: Cached at the CDN edge for 24 hours */}
<ProductHero slug={params.slug} />
<ProductDescription slug={params.slug} />
{/* 2. Asymmetric Micro-ISR Slot: Revalidates vertically every 60s */}
<Suspense fallback={<StockSkeleton />}>
<DinouCacheSlot
id={`stock-${params.slug}`}
tag={`stock-${params.slug}`}
revalidate={60}
>
<LiveStockCounter slug={params.slug} />
</DinouCacheSlot>
</Suspense>
</main>
);
}
[!IMPORTANT] Boundary Placement: Why
<Suspense>Must Wrap<DinouCacheSlot>
<DinouCacheSlot>is itself an asynchronous Server Component. When it awaits storage or renders on a cache miss, React encounters a pending promise in the parent page tree. Without an outer<Suspense>, suspension bubbles to the root, blocking the entire HTTP response and delaying fast sibling components.The Rule of Hierarchy:
<Suspense fallback={<Skeleton />}>→ Controls browser streaming & instant visual layout.<DinouCacheSlot revalidate={300}>→ Controls server cache storage, TTL, and cache tags.<SlowAnalyticsWidget />→ Executes database queries and business logic.
Dinou provides specialized mechanisms to refresh or purge <DinouCacheSlot> instances without reloading the browser page:
refreshSlot)Any interactive Client Component can trigger a surgical re-render of only that slot without full page reload or state loss:
"use client";
import { refreshSlot, useRouter } from "dinou";
export function RefreshButton() {
const router = useRouter();
return (
<div className="flex gap-2">
{/* Option 1: Standalone (respects server slot TTL) */}
<button onClick={() => refreshSlot("analytics-widget")}>
Refresh Metrics
</button>
{/* Option 2: Force fresh (bypasses server slot TTL immediately) */}
<button onClick={() => router.refreshSlot("analytics-widget", { fresh: true })}>
Force Fresh
</button>
</div>
);
}
refreshSlot(id) (Default): Refreshes the client DOM with the server's current state. If the slot's server TTL has not expired, it returns cached server JSX without touching your database (protecting against button spam and traffic spikes).refreshSlot(id, { fresh: true }) (Bypass): Hard revalidation. Forces the server to ignore unexpired TTL, immediately re-run queries against the database, and update both cache and client.revalidateTag)Mutations purge slots globally via revalidateTag() on the server, while the client immediately pulls the fresh result via refreshSlot():
// src/app/feed/actions.ts
"use server";
import { revalidateTag } from "dinou/server";
import db from "@/lib/db";
export async function publishPost(formData: FormData) {
const content = formData.get("content") as string;
await db.posts.insert({ content, createdAt: new Date() });
// Purges cache globally for ALL future visitors across CDNs and Micro-ISR slots
await revalidateTag("feed");
}
startTransition + Stale-Preserving Fallback)While the server re-renders an expired slot during an interactive refresh, the user does NOT see a blank skeleton flash or layout shift. Dinou's internal client boundary wraps slot updates in React.startTransition and sets fallback: children (the previous DOM content). The existing widget stays interactive and visible until the new server stream arrives, then swaps seamlessly.
To display an inline loading indicator on the button itself, wrap refreshSlot inside React's standard useTransition():
"use client";
import { useTransition } from "react";
import { refreshSlot } from "dinou";
export function RefreshAnalyticsButton() {
const [isPending, startTransition] = useTransition();
const handleRefresh = () => {
startTransition(() => {
refreshSlot("analytics-widget");
});
};
return (
<button onClick={handleRefresh} disabled={isPending}>
{isPending ? "Refreshing Metrics..." : "Refresh Metrics"}
</button>
);
}
| Dimension | Scope | Configuration | Best For |
|---|---|---|---|
| Horizontal Segmentation | Route / Segment Level | page_functions.ts, layout_functions.ts | Whole-page SSG, ISR, and layout persistence. |
| Vertical Segmentation | Component Subtree Level | <DinouCacheSlot> from dinou/server | Independent widget TTLs, asymmetric revalidation, and in-place client refreshes. |
<DinouCacheSlot>page.tsx body inside a slot reinvents horizontal caching. Declare export const revalidate = 300 in page_functions.ts instead.<DinouCacheSlot>Nesting <DinouCacheSlot> inside a React <Suspense> boundary on a route with Partial Prerendering (PPR) unlocks the highest performance profile possible: zero-delay initial delivery (0ms TTFB) paired with near-zero streaming delay (~2ms) and 99.9% database query offloading.
<DinouCacheSlot> (Cached Streamed Hole): 0ms static shell from Edge CDN + hole resolves from server cache in ~2ms. 10,000 visitors/min = 1 database query per TTL window (99.9% database offload).// src/telemetry/page_functions.ts
import type { PageFunctions } from "dinou";
// Enable Partial Prerendering on this high-traffic route
export const ppr: PageFunctions["ppr"] = true;
// src/telemetry/page.tsx
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou/server";
import { TelemetryHeader, TelemetryFooter } from "./static-shell";
import { HeavyMetricsGraph, GraphSkeleton } from "./graph-widgets";
export default function TelemetryPage() {
return (
<main className="container py-8 space-y-6">
{/* 1. Static Shell: Delivered instantly in 0ms TTFB from Edge CDN */}
<TelemetryHeader />
{/* 2. PPR Dynamic Hole + Micro-ISR Slot:
- Fallback skeleton sent immediately in initial layout
- Hole streams from server cache in ~2ms for subsequent visitors
- Database query runs only ONCE every 60 seconds */}
<Suspense fallback={<GraphSkeleton />}>
<DinouCacheSlot id="operations-telemetry" tag="telemetry" revalidate={60}>
<HeavyMetricsGraph />
</DinouCacheSlot>
</Suspense>
<TelemetryFooter />
</main>
);
}
While ISR updates existing pre-rendered pages, ISG (Incremental Static Generation) generates new dynamic paths on demand that were omitted from the build-time getStaticPaths list.
Any route path omitted from getStaticPaths will be generated dynamically when the first visitor requests it, cached, and then served as static HTML to subsequent visitors:
// src/products/[id]/page_functions.ts
// Pre-render only the top 10 products at build time:
export const getStaticPaths = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
// Any other ID (e.g. /products/452) will be generated on-demand
// on its first visitor and cached permanently.
validateParams & allowISG)Dinou provides strict server-side governance exports to secure on-demand generation against cache pollution and DDoS:
validateParams(params): Validates route parameters before generation begins. Returning false halts rendering immediately and serves an instant 404:
export function validateParams(params: { id: string }) {
return /^\d+$/.test(params.id); // Only allow numeric IDs
}
allowISG: Disables on-demand generation entirely. When set to false, any path not pre-rendered in getStaticPaths returns 404 without querying the server:
export const allowISG = false; // Strict SSG only
Summary of all rendering strategies supported natively by Dinou v7:
| Strategy | How to Configure | TTFB | Freshness | Database Load | Best For |
|---|---|---|---|---|---|
| Pure SSG | Default / revalidate = false | 0ms (Instant) | Build-time | 0 queries (Build-time only) | Landing pages, marketing, documentation |
| ISR | revalidate = N (N >= 1) | 0ms (Cached) | Stale-while-revalidate | 1 query per TTL (0 on cache hits) | Blogs, catalogs, news feeds |
| Dynamic SSR | revalidate = 0 | Request-time (Blocked) | 100% Real-time | Heavy: 100% queries on every request | User settings, banking, checkout |
| PPR (Partial Prerendering) | ppr = true + <Suspense> | 0ms (Shell from Edge) | 100% Real-time (Holes) | Heavy: 100% Hole queries on every request | E-commerce, dashboards with static shells |
| Micro-ISR (Static / Asymmetric ISR) | <DinouCacheSlot> (in Static/ISR page) | 0ms (CDN Edge) | Decoupled Slot TTL / Tags | Low: Isolated to expired slots | Static product/catalog pages with volatile widgets (stock, prices) |
| Micro-ISR (Dynamic SSR) | <DinouCacheSlot> (in Dynamic SSR page) | Fast (Early Layout / Slot-cached) | Component TTL / Tags | Low: Isolated to expired slots | Dynamic user dashboards with heavy SQL calculations |
| PPR + Micro-ISR (Synergy) | ppr = true + <DinouCacheSlot> | 0ms (Edge) + ~2ms (Hole) | Cached Streamed Hole | Near-Zero: 1 query per slot TTL (99.9% reduction) | High-concurrency e-commerce, telemetry dashboards |
| ISG (Incremental Static) | allowISG = true (default) | 0ms (after 1st hit) | Cached permanently | 1 query on 1st generation (0 thereafter) | Long-tail e-commerce products, dynamic paths |
[!TIP] For deep-dive code recipes, runtime execution timing, TTFB charts, and production trade-offs across all 13 rendering and data fetching patterns, see Data Fetching: The 13 Strategies below.
Server Functions are asynchronous RPC endpoints that run exclusively in the server runtime. In Dinou, you declare them by adding the "use server" directive at the top of a dedicated action file (module-level directive).
| Capability | Architectural Benefit |
|---|---|
| Zero API Boilerplate | No manual route handlers, fetch() boilerplate, or JSON serialization required. Dinou generates typed Flight RPC endpoints (POST /____server_function____) automatically. |
| JSX & Data Returns | Unlike standard RPCs that only return JSON, Dinou Server Functions can return fully rendered React components streamed over Flight RPC (The Dinou Pattern). |
| Security & Secrets | Secret keys, database connections, and ORM calls are never bundled into the client-side JavaScript chunk. |
| Form & Action Synergy | Native integration with HTML <form action={...}> and React 19 primitives (useActionState, useFormStatus, useOptimistic). |
Dinou uniquely allows Server Functions to return fully rendered React component trees directly over Flight RPC. In Server Components, the component is awaited directly inside native <Suspense>; in Client Components, the returned promise is unwrapped with React 19's use() hook:
// src/actions/get-post-card.tsx
"use server"; // 👈 Module-level directive in Dinou
import db from "@/lib/db";
import { PostCard } from "@/components/post-card";
export async function getPostCard(postId: string) {
const post = await db.posts.findUnique({ where: { id: postId } });
if (!post) throw new Error("Post not found");
// Returns a rendered JSX component directly over Flight RPC!
return <PostCard post={post} />;
}
Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve:
// src/blog/page.tsx
// Server Component: directly await the Server Function with React 19 Suspense
import { Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";
async function PostSlot({ id }: { id: string }) {
// Directly awaited on the server during SSR / streaming
return await getPostCard(id);
}
export default function BlogPage() {
return (
<section className="space-y-4">
<h1 className="text-2xl font-bold">Featured Story</h1>
{/* Native React 19 Suspense fallback */}
<Suspense fallback={<div className="skeleton h-48 w-full rounded" />}>
<PostSlot id="post-101" />
</Suspense>
</section>
);
}
use())Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries, and re-suspends cleanly on promise update:
// src/blog/page.tsx
// Client Component: unwrap the Flight RPC promise with React 19 use()
"use client";
import { useState, use, Suspense } from "react";
import { getPostCard } from "@/actions/get-post-card";
function PostStream({ promise }: { promise: Promise<React.ReactNode> }) {
// Pure React 19 'use()' unrolls the promise within Suspense
const content = use(promise);
return <>{content}</>;
}
export default function BlogPage() {
const [postId, setPostId] = useState("post-1");
const [postPromise, setPostPromise] = useState(() => getPostCard("post-1"));
const loadPost = (id: string) => {
setPostId(id);
setPostPromise(getPostCard(id));
};
return (
<section className="space-y-4">
<h1 className="text-2xl font-bold">Featured Story</h1>
<div className="flex gap-2">
<button onClick={() => loadPost("post-1")} className="btn">Post 1</button>
<button onClick={() => loadPost("post-2")} className="btn">Post 2</button>
</div>
{/* Re-suspends cleanly on promise update */}
<Suspense key={postId} fallback={<p>Streaming post from server...</p>}>
<PostStream promise={postPromise} />
</Suspense>
</section>
);
}
[!NOTE] Execution Comparison: Server vs. Client Components
- In Server Components (Async / Await): Directly
awaitthe Server Function inside an async component. React streams HTML progressively as promises resolve.- In Client Components (with
use()): Dispatches Flight RPC returning a Promise. React 19's nativeuse(promise)unrolls it synchronously inside<Suspense>without external state libraries.
Dinou integrates seamlessly with standard HTML forms and React 19 form actions for data mutations.
You can pass a Server Action directly to the native HTML <form action={...}> attribute:
// src/components/newsletter-page.tsx
"use client";
import { subscribeNewsletter } from "@/actions/subscribe";
export default function NewsletterPage() {
return (
<form action={subscribeNewsletter} className="flex gap-2">
<input name="email" type="email" placeholder="[email protected]" required className="input" />
<button type="submit" className="btn-primary">Subscribe</button>
</form>
);
}
// src/actions/subscribe.ts
"use server"; // 👈 Required at the module level in Dinou
import db from "@/lib/db";
export async function subscribeNewsletter(formData: FormData) {
const email = formData.get("email") as string;
await db.subscribers.create({ data: { email } });
}
[!NOTE] Module-Level Directives & Client Execution Dinou requires the
"use server"directive to be placed at the top of the action file (not inline within function bodies). When a form submits, Dinou's client runtime transparently dispatches a Flight RPC request (POST /____server_function____) to execute the server action and receive updated UI state.
[!TIP] What does
subscribeNewsletterreturn? (Understandingvoid/undefined) This action returnsPromise<void>, which resolves toundefined. In pure React 19 form actions (<form action={subscribeNewsletter}>), this is completely standard for fire-and-forget mutations: the browser submits the form data, the server executes the database write, and no state payload is needed by the UI.When should a Server Action return a value? If your UI needs to display interactive feedback (such as "Subscribed successfully!" or validation errors), the action should return a serializable payload (e.g.
{ success: true, message: "..." }) and be wired to React 19'suseActionStatehook (as shown below in Section 2.2). Other common patterns include performing a server-sideredirect()or callingrevalidatePath()before completing.
For rich interactive forms with validation feedback and loading spinners, use React 19's standard hooks:
useActionState (React 19): Captures the returned state (errors, success payload) and pending status of the Server Action across submissions.useFormStatus (react-dom): Reads the pending submission status of the parent <form> without prop drilling.// src/components/create-post-form.tsx
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createPostAction, type FormState } from "@/actions/posts";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} className="btn-primary">
{pending ? "Saving..." : "Publish Post"}
</button>
);
}
const initialState: FormState = { success: false, message: "" };
export function CreatePostForm() {
// useActionState binds the server action to form state and tracks submission
const [state, formAction, isPending] = useActionState(createPostAction, initialState);
return (
<form action={formAction} className="flex flex-col gap-4">
{state.message && (
<div className={state.success ? "text-green-500" : "text-red-500"}>
{state.message}
</div>
)}
<input name="title" placeholder="Post title" required className="input" />
<textarea name="content" placeholder="Write content..." required className="textarea" />
<SubmitButton />
</form>
);
}
// src/actions/posts.ts
"use server"; // 👈 Module-level directive required in Dinou
import db from "@/lib/db";
export type FormState = {
success: boolean;
message: string;
};
// Server Action receives (prevState, formData) when bound to useActionState
export async function createPostAction(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const title = (formData.get("title") as string)?.trim();
const content = (formData.get("content") as string)?.trim();
if (!title || title.length < 3) {
return { success: false, message: "Title must be at least 3 characters." };
}
if (!content) {
return { success: false, message: "Content cannot be empty." };
}
await db.posts.create({ data: { title, content } });
return { success: true, message: "Post published successfully!" };
}
With React 19's useOptimistic, you can update the user interface immediately before the server responds:
// src/components/like-button.tsx
"use client";
import { useOptimistic, useTransition } from "react";
import { toggleLikeAction } from "@/actions/likes";
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [isPending, startTransition] = useTransition();
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
initialLikes,
(current, update: number) => current + update
);
const handleClick = () => {
startTransition(async () => {
setOptimisticLikes(1); // Optimistically increment in 0ms on client
await toggleLikeAction(postId); // Dispatches Server Action via Flight RPC
});
};
return (
<button onClick={handleClick} disabled={isPending} className="btn-like">
❤️ {optimisticLikes}
</button>
);
}
// src/actions/likes.ts
"use server"; // 👈 Module-level directive required in Dinou
import db from "@/lib/db";
export async function toggleLikeAction(postId: string) {
// Updates like count on the database
await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
return { success: true };
}
[!NOTE] Why is there no
<Suspense>boundary here?<Suspense>only activates when a component suspends (by throwing a pending Promise, such as withReact.use(promise)or an async Server Component).In contrast,
useOptimisticdoes the exact opposite of suspending: it synchronously paints the temporary new value (e.g. switching immediately from10to11) in 0ms on the client, avoiding loading spinners or skeleton fallbacks entirely while the Server Action runs in the background. It relies strictly on React 19 transitions (useTransitionor Form Actions). If the server call succeeds, React seamlessly commits the real server state; if it fails, React automatically rolls back to the initial value with zero boilerplate.
Dinou provides getContext() (exported from "dinou") to access the underlying request and response lifecycle synchronously within any Server Function or SSR component.
RequestContextStore API Reference:
context.req.cookies: Dictionary of incoming client cookies (e.g. cookies["session_token"]).context.req.headers: Incoming HTTP headers dictionary (e.g. headers["user-agent"]).context.req.query: URL search parameters.context.res.cookie(name, value, options): Sets an HTTP cookie with options (httpOnly, secure, maxAge, sameSite, path, etc.).context.res.clearCookie(name, options): Clears a cookie from the client.// src/actions/auth-check.ts
"use server";
import { getContext } from "dinou";
export async function getCurrentUser() {
const context = getContext();
if (!context) return null;
const sessionToken = context.req.cookies["session_token"];
const userAgent = context.req.headers["user-agent"];
if (!sessionToken) return null;
return await verifySession(sessionToken);
}
// src/actions/login.ts
"use server";
import { getContext } from "dinou";
export async function loginAction(formData: FormData) {
const context = getContext();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const session = await authenticateUser(email, password);
// Set secure HTTP-only session cookie
context?.res.cookie("session_token", session.token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
path: "/",
});
return { success: true };
}
export async function logoutAction() {
const context = getContext();
// Clear the cookie upon logout
context?.res.clearCookie("session_token", { path: "/" });
return { success: true };
}
Dinou provides a universal redirect(destination) utility exported from "dinou":
router.replace()) without reloading the page.// src/actions/checkout.ts
"use server";
import { redirect } from "dinou";
import db from "@/lib/db";
export async function completeOrder(orderId: string) {
await db.orders.updateStatus(orderId, "PAID");
// Automatically navigates the client router to the success page
return redirect(`/checkout/${orderId}/success`);
}
A foundational question when handling redirects across hybrid architectures is: How does Dinou determine whether redirect() issues an HTTP 302 vs. a client-side transition, and is calling redirect() in a Server Component equivalent to invoking it via a Server Function?
[!NOTE] The Equivalence Principle in Server Components Calling
redirect()directly inside a Server Component vs. calling an imported Server Function (e.g.,await verifyUser()) from that Server Component is functionally identical. Because both run inside the same server Node.js process during the same SSR request lifecycle, they share the exact same request context and streaming state.
Occurs inside getProps() in page_functions.ts, at the top level of the root page.tsx Server Component (outside any <Suspense> boundary), or in a global plugin hook (onRequest) in dinou.config.mjs. Because zero HTML bytes have reached the wire (headersSent === false), Dinou issues a clean HTTP 302 with a Location header. Crucial for SEO crawlers and search indexers.
// src/dashboard/page_functions.ts
import { redirect, getContext } from "dinou";
export async function getProps() {
const { req } = getContext();
if (!req.cookies.session_token) {
// Evaluated before page rendering -> HTTP 302 redirect
return redirect("/login");
}
return { user: { name: "Alex" } };
}
// src/dashboard/page.tsx (Server Component root)
import { redirect, getContext } from "dinou";
export default async function DashboardPage() {
const { req } = getContext();
if (!req.cookies.session_token) {
// At root before stream begins -> HTTP 302 redirect
return redirect("/login");
}
return <h1>Welcome to Dashboard</h1>;
}
// dinou.config.mjs (Global Plugin Hook)
import { defineConfig } from "dinou/config";
export default defineConfig({
plugins: [
{
name: "auth-guard",
async onRequest(request, context) {
const url = new URL(request.url);
const cookieHeader = request.headers.get("cookie") || "";
// Runs pre-flight before SSR or route resolution
if (url.pathname.startsWith("/dashboard") && !cookieHeader.includes("session_token=")) {
return Response.redirect(new URL("/login", request.url), 302);
}
},
},
],
});
Occurs inside an async child wrapped in <Suspense fallback={<Skeleton />}>. React opens the HTTP connection with status 200 OK immediately to stream the initial shell. At HTTP protocol level, an open 200 OK stream cannot turn into a 302. Dinou safely emits <ClientRedirect />, completing the transition upon browser hydration without breaking the stream:
// src/reports/page.tsx (Server Component)
import { Suspense } from "react";
import { redirect } from "dinou";
async function RestrictedReport() {
const isAllowed = await checkUserAccess();
if (!isAllowed) {
// 200 OK stream already open -> <ClientRedirect />
return redirect("/unauthorized");
}
return <div>Financial Metrics</div>;
}
export default function Page() {
return (
<Suspense fallback={<p>Loading financial report...</p>}>
<RestrictedReport />
</Suspense>
);
}
Triggered from browser interactions (form actions or button clicks) via a dedicated POST request. Dinou executes the server mutation and intercepts redirect() before streaming (headers unsent), returning redirect metadata. The client proxy navigates via the SPA router without a full page reload:
// src/actions/cart.ts
"use server";
import { redirect } from "dinou";
import db from "@/lib/db";
export async function checkoutAction(formData: FormData) {
const order = await db.orders.create({ /* ... */ });
// Dedicated POST: soft SPA navigation in browser without page reload
return redirect(`/orders/${order.id}`);
}
Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms:
// src/components/client-guard.tsx
"use client";
import { redirect } from "dinou";
export function ClientGuard({ isAllowed }: { isAllowed: boolean }) {
if (!isAllowed) {
// In-browser render: executes router.replace() in SPA router
return redirect("/forbidden");
}
return <button className="btn">Control Panel</button>;
}
| Invocation Context | Headers Sent? | Resulting Mechanism | Recommended For |
|---|---|---|---|
| 1. SSR Root ( page_functions.ts, root page.tsx, Plugin) | No | Hard HTTP Redirect (Status 302) Server issues an HTTP/1.1 302 Found with a Location header. The browser or search crawler intercepts it at the protocol level and fetches the new page as a fresh document. | Auth guards (getProps), root guards, global middleware plugins, SEO canonicals |
| 2. SSR: Deferred ( <Suspense>) | Yes (200 OK stream open) | Streaming Fallback (<ClientRedirect />)Because the 200 OK chunked stream cannot be rewritten to a 302, Dinou injects <ClientRedirect /> into the stream. The browser router transitions upon hydration without aborting the connection. | Deferred conditional access, late data-dependent checks inside Suspense |
| 3. Server Function (from Client Action) | No (standard POST) | Soft SPA Navigation (Status 200) Server returns HTTP 200 OK with redirection metadata. Dinou's client runtime intercepts it and navigates via the SPA router without a full page reload or loss of client state. | Form submissions, shopping cart checkout, stateful mutations |
| 4. Client Component (Render) | N/A (in-browser) | Client Router Replacement (router.replace())Executes 100% in the browser with zero server roundtrip. Immediately dispatches router.replace() in the client SPA router, updating route and UI in 0ms. | Pure UI state redirection, client-side permission gates |
[!TIP] Looking for Cache Revalidation? Learn how to purge stale SSG pages and ISR cache slots on-demand using
revalidatePathandrevalidateTagin On-Demand Revalidation below.
High-performance web applications cannot rely exclusively on time-based expiration (TTL). On-Demand Revalidation in Dinou enables instantaneous cache purging and static regeneration across four specialized tiers:
| Invalidation Tier | API & Mechanism | Architectural Scope |
|---|---|---|
| Surgical Page | revalidatePage(path) | Regenerates exclusively page.rsc and index.html, leaving all parent layout shells completely untouched. |
| Route Segment | revalidatePath(path) | Purges page RSC, HTML, and any layout.rsc in that segment. Supports recursive propagation with { cascade: true }. |
| Cross-Cutting Tag | revalidateTag(tag) | Selectively updates layouts, pages, or micro-slots across disparate routes sharing a common metadata tag key. |
| Micro-Slot | <DinouCacheSlot> | Isolates volatile component subtrees refreshed independently via refreshSlot() or targeted revalidateTag(). |
Server revalidation APIs are exported from the server-only endpoint "dinou/server", keeping browser client bundles clean, lightweight, and secure.
[!CAUTION] Strict Execution Rules: Where to Call Revalidation APIs
- Only in Mutative Event Contexts: Revalidation APIs (
revalidatePage,revalidatePath,revalidateTag) are mutative side-effects. They must only be invoked inside Server Functions ("use server") or HTTP Webhook endpoints (such as Headless CMS publishing events).- Never Inside Components or
getProps: Calling revalidation during the component render phase or withingetPropscreates an infinite regeneration loop, causing severe CPU exhaustion and server crashes.
Exported from "dinou/server", revalidatePage(path) is specifically designed for scenarios where only the leaf page content has changed. It provides two critical advantages:
Truncated — view the full README on GitHub.
220 followers · starred Jun 2026