Server Component Props
Serializable props across the server/client boundary, Server Actions, and the donut pattern.
// ServerPage.tsx (Server Component)
import { saveToDatabase } from './db';
<ClientForm
onSubmit={(data: FormData) => {
saveToDatabase(data);
}}
/>// actions.ts
'use server';
export async function saveForm(data: FormData) {
await saveToDatabase(data);
}
// ServerPage.tsx (Server Component)
import { saveForm } from './actions';
<ClientForm action={saveForm} />Props from Server Components to Client Components are serialized over the network. Regular functions can't survive this, and they crash at runtime.
'use server' is the key: it turns the function into a serializable reference (essentially a URL). The actual code stays on the server; the client gets a reference it can call over the network.
// page.tsx (Server Component)
<AnimatedContainer
title={article.title}
body={article.body}
author={article.author}
publishedAt={article.publishedAt}
/>// page.tsx (Server Component)
<AnimatedContainer>
<ArticleContent article={article} />
</AnimatedContainer>This is called the "donut pattern": the Client Component is the outer ring (handles animation), and Server Component children pass through the hole in the middle. AnimatedContainer never touches the article data, it just renders {children}.
ArticleContent stays a Server Component: all article data is rendered on the server and never serialized to the client. The client only ships the animation JavaScript.
'use client';
interface ContactFormProps {
/** Called when the form is submitted. */
onSubmit: (data: FormData) => Promise<void>;
/** Shows a loading spinner. */
isPending: boolean;
}'use client';
interface ContactFormProps {
/**
* Server Action invoked on form submission.
* Passed to <form action={...}>.
*/
action: (formData: FormData) => Promise<void>;
}
// isPending comes from useActionState:
// const [state, formAction, isPending] =
// useActionState(action, initialState);React 19's <form action={...}> pattern handles pending state automatically via useActionState or useFormStatus. Passing isPending as a prop duplicates what the framework already provides.
Naming the prop action (not onSubmit) signals it's a Server Action, not a client-side event handler.
// Server Component
<ClientEditor
pattern={highlightPattern}
metadata={documentMeta}
/>
// Client Component
interface ClientEditorProps {
pattern: RegExp;
metadata: DocumentMeta; // class instance
}// Server Component
<ClientEditor
pattern={highlightPattern.source}
metadata={{
title: documentMeta.title,
wordCount: documentMeta.wordCount,
}}
/>
// Client Component
interface ClientEditorProps {
pattern: string;
metadata: { title: string; wordCount: number };
}RegExp and class instances are not serializable across the server/client boundary. Convert to serializable equivalents: RegExp → its .source string, class instances → plain objects with only the needed fields.
Serializable types include: primitives, Date, Map, Set, BigInt, typed arrays, plain objects, and arrays. NOT serializable: RegExp, class instances, WeakMap, Symbol, and functions.
'use client';
interface ProductPageProps {
product: {
id: string;
name: string;
price: number;
description: string;
reviews: Review[];
};
onAddToCart: () => void;
onToggleWishlist: () => void;
}// ProductPage (Server Component)
interface ProductPageProps {
product: {
id: string;
name: string;
price: number;
description: string;
reviews: Review[];
};
}
// AddToCartButton (Client Component)
interface AddToCartButtonProps {
productId: string;
action: (id: string) => Promise<void>;
}Making the entire page a Client Component just because two buttons need interactivity forces all product data to be serialized and sent to the client. Instead, keep the page as a Server Component and push 'use client' to the smallest leaf components.
Only the button needs to be a Client Component, and it receives just a productId and a Server Action, not the full product object.