Server Functions & Actions
Execute asynchronous server logic directly from your components with seamless Flight RPC. Dinou uniquely allows Server Functions to return live, rendered React components as well as serializable data, with native React 19 form actions and hooks.
Overview & "use server" Directive
Server Functions are asynchronous JavaScript/TypeScript functions 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).
fetch() boilerplate, or JSON serializing required. Dinou generates typed RPC endpoints automatically.1. Returning Live React Components (UI Streaming)
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} />;
}// 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>
);
}// 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>
);
}Directly await the Server Function inside an async component. React streams HTML progressively as promises resolve.
use())Dispatches Flight RPC returning a Promise. React 19's native use(promise) unrolls it synchronously inside <Suspense> without external state libraries.
2. Server Actions & Form Mutations
Server Functions passed to the action prop of a <form> or formAction of a button act as Server Actions.
2.1. Server Actions in Forms
In Dinou, Server Actions are declared in dedicated server files with the "use server" directive at the top. You can pass them directly to the action attribute of a <form> in both Server Components and Client Components:
// src/newsletter/page.tsx
// Server Component (invoking Server Action directly)
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 } });
}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.
subscribeNewsletter return? (Understanding void / undefined)This action returns Promise<void>, which resolves to undefined. 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's useActionState hook (as explained in Section 2.2 below). Other common patterns include performing a server-side redirect() or calling revalidatePath() before completing.
2.2. React 19 Hooks: useActionState & useFormStatus
For rich interactive forms with validation feedback and loading spinners, use React 19's standard hooks:
<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!" };
}2.3. Optimistic Updates with useOptimistic
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 };
}<Suspense> boundary here?<Suspense> only activates when a component suspends (by throwing a pending Promise, such as with React.use(promise) or an async Server Component).
In contrast, useOptimistic does the exact opposite of suspending: it synchronously paints the temporary new value (e.g. switching immediately from 10 to 11) 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 (useTransition or 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.
3. Request Context, Cookies & Headers (getContext)
Dinou provides getContext() (exported from "dinou") to access the underlying request and response lifecycle synchronously within any Server Function or SSR component.
context.req.cookies: Dictionary of incoming client cookies (e.g.cookies["auth_token"]).context.req.headers: Incoming HTTP headers dictionary.context.req.query: URL search parameters.context.res.cookie(name, value, options): Sets an HTTP cookie with options (httpOnly,secure,maxAge, etc.).context.res.clearCookie(name, options): Clears a cookie from the client.
3.1. Reading Query, Headers & Cookies
// 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);
}4. Redirections (redirect)
Dinou provides a universal redirect(destination) utility exported from "dinou":
- In Server Functions (Soft SPA Navigation): When called inside a Server Function (e.g. form mutations or RPC calls), Dinou automatically performs a smooth client-side transition to the destination route via the SPA router without a full page reload.
- In Server Components (Initial SSR): If headers have not yet been sent, Dinou issues a clean HTTP 302 redirect for SEO and search engine crawlers. If response streaming has already started, it completes the transition on the client upon hydration.
- In Client Components (Declarative Redirects): When called directly in client components during render, it triggers an immediate client router replacement (
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?
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
return redirect(`/checkout/${order.id}/success`);
}When called directly inside a client component during browser rendering, no network request is sent to the server.
Dinou immediately delegates to router.replace() in the client SPA router, smoothly updating the URL and view in 0ms without a page reload.
// 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, page.tsx, Plugin) | No | Hard HTTP Redirect (Status 302) Server issues an | Auth guards (getProps), root guards, global middleware plugins, SEO canonicals |
| 2. SSR: Deferred (<Suspense>) | Yes(200 OK stream open) | Streaming Fallback ( <ClientRedirect />)Because the | 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 | 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 | Pure UI state redirection, client-side permission gates |
Learn how to purge stale SSG pages and ISR cache slots on-demand using revalidatePath and revalidateTag in the dedicated guide.