Learn

/

Server Component Props

Server Component Props

5 patterns

Serializable props across the server/client boundary, Server Actions, and the donut pattern.

Avoid
// ServerPage.tsx (Server Component)
import { saveToDatabase } from './db';

<ClientForm
  onSubmit={(data: FormData) => {
    saveToDatabase(data);
  }}
/>

Prefer
// 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.

React Docs: Server Actions
Avoid
// page.tsx (Server Component)

<AnimatedContainer
  title={article.title}
  body={article.body}
  author={article.author}
  publishedAt={article.publishedAt}
/>

Prefer
// 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.

Next.js: Server and Client Components
Avoid
'use client';

interface ContactFormProps {
  /** Called when the form is submitted. */
  onSubmit: (data: FormData) => Promise<void>;
  /** Shows a loading spinner. */
  isPending: boolean;
}

Prefer
'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.

React Docs: useActionState
Avoid
// Server Component
<ClientEditor
  pattern={highlightPattern}
  metadata={documentMeta}
/>

// Client Component
interface ClientEditorProps {
  pattern: RegExp;
  metadata: DocumentMeta; // class instance
}

Prefer
// 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.

React Docs: Serializable Types
Avoid
'use client';

interface ProductPageProps {
  product: {
    id: string;
    name: string;
    price: number;
    description: string;
    reviews: Review[];
  };
  onAddToCart: () => void;
  onToggleWishlist: () => void;
}

Prefer
// 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.

Next.js: Server and Client Components