Skip to content

Composing Server and Client Components — Architecture Patterns

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Composing Server and Client Components. We cover key concepts, practical examples, and best practices to help you master this topic.

Composing Server and Client Components requires understanding how they interact, what crosses the boundary, and which patterns keep your architecture clean.

What You'll Learn

You will learn composition patterns for mixing Server and Client Components, how to pass data across the boundary, and how to structure component hierarchies.

Why It Matters

Poor composition leads to bloated bundles, broken hydration, and hard-to-maintain code. Clean composition patterns maximize the benefits of both component types.

Real-World Use

DodaTech's tutorial platform uses a Server Component shell for the page layout and SEO metadata, with Client Components embedded for the search bar, code sandbox, and interactive quiz elements.

flowchart TD
    A[Page Server Component] --> B[Header Client]
    A --> C[Content Server]
    A --> D[Sidebar Client]
    C --> E[Interactive Code Sandbox Client]
    C --> F[Article Text Server]
    C --> G[Like Button Client]
    D --> H[Table of Contents Server]
    D --> I[Search Client]
    style A fill:#1e293b,color:#fff
    style C fill:#1e293b,color:#fff
    style B fill:#0f172a,color:#fff
    style D fill:#0f172a,color:#fff

The Wrapper Pattern

Use a Client Component as an interactive wrapper around Server Component content.

'use client';
function ExpandableCard({ title, children }) {
  const [expanded, setExpanded] = useState(false);
  return (
    <div style={{ border: '1px solid #ccc', borderRadius: '8px', margin: '8px 0' }}>
      <button onClick={() => setExpanded(!expanded)} style={{ width: '100%', padding: '12px', textAlign: 'left' }}>
        {title} {expanded ? '▲' : '▼'}
      </button>
      {expanded && <div style={{ padding: '12px' }}>{children}</div>}
    </div>
  );
}

// Server Component using the wrapper
async function FAQPage() {
  const faqs = await db.faqs.findAll();
  return (
    <div>
      <h1>Frequently Asked Questions</h1>
      {faqs.map(faq => (
        <ExpandableCard key={faq.id} title={faq.question}>
          <p>{faq.answer}</p>
        </ExpandableCard>
      ))}
    </div>
  );
}

Expected output: The FAQ page renders as a list of expandable cards. The card wrapper is a Client Component handling the toggle state. The content inside each card is static HTML from the Server Component.

Passing Client Components as Props

Server Components can pass Client Components as props to other Server Components.

'use client';
function DeleteButton({ itemId, onDelete }) {
  const [confirming, setConfirming] = useState(false);
  return (
    <span>
      <button onClick={() => setConfirming(true)}>Delete</button>
      {confirming && (
        <span>
          <button onClick={() => onDelete(itemId)}>Confirm</button>
          <button onClick={() => setConfirming(false)}>Cancel</button>
        </span>
      )}
    </span>
  );
}

// Server Component
async function ItemList({ renderActions }) {
  const items = await db.items.findAll();
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          {renderActions(item)}
        </li>
      ))}
    </ul>
  );
}

// Page combines them
export default function Page() {
  return (
    <ItemList
      renderActions={(item) => <DeleteButton itemId={item.id} />}
    />
  );
}

Expected output: A list of items with a delete button next to each. Clicking Delete shows a confirm/cancel prompt. The DeleteButton is a Client Component passed as a render prop to the Server Component.

Lifting Interactive Boundaries

When a Server Component needs interactivity, lift the interactive part into a separate Client Component.

// Instead of making the whole page a Client Component, extract the interactive part
'use client';
function SearchSection({ initialData }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState(initialData);

  async function handleSearch(e) {
    const q = e.target.value;
    setQuery(q);
    if (q.length > 2) {
      const res = await fetch(`/api/search?q=${q}`);
      setResults(await res.json());
    } else {
      setResults(initialData);
    }
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="Search..." />
      <ul>
        {results.map(r => <li key={r.id}>{r.title}</li>)}
      </ul>
    </div>
  );
}

// Page stays as Server Component
async function SearchPage() {
  const initialData = await db.items.findRecent();
  return (
    <div>
      <h1>Search</h1>
      <SearchSection initialData={initialData} />
    </div>
  );
}

Expected output: The page shell is a Server Component fetching initial data. The SearchSection is a Client Component that handles user input and filtering. Only the interactive search logic ships to the client.

Server Components Inside Client Components

Pass Server Components as children to Client Components using the children prop.

'use client';
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  return (
    <div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.5)' }}>
      <div style={{ background: 'white', padding: '24px', margin: '10% auto', maxWidth: '500px' }}>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

// Usage
async function ProductPage({ params }) {
  const product = await db.products.findById(params.id);
  return (
    <div>
      <h1>{product.name}</h1>
      <Modal isOpen={true}>
        <h2>Product Details</h2>
        <p>{product.description}</p>
        <p>Price: ${product.price}</p>
      </Modal>
    </div>
  );
}

Expected output: The Server Component renders product data inside a Modal Client Component. The modal content is Server-rendered HTML passed as children. The modal's open/close state is managed client-side.

Common Mistakes

  1. Making entire pages Client Components: Only the interactive parts need 'use client'. Keep the page shell and data-fetching sections as Server Components.

  2. Trying to import Server Components into Client Components: This is not allowed. Pass Server Components as children or props instead.

  3. Creating unnecessary Serialization boundaries: Every boundary between Server and Client serializes props. Keep boundaries where they make sense, not everywhere.

  4. Putting context providers in Server Components: Context providers must be in Client Components because they manage state. Wrap the app in a Client Component provider.

  5. Not extracting interactive leaf components early: When a Server Component grows interactive needs, extract the interactive part into a separate Client Component file.

Practice Questions

  1. How do you pass a Server Component inside a Client Component?

Pass it as the children prop or as a render prop. The Client Component renders it without knowing it is a Server Component.

  1. What is the wrapper pattern?

A Client Component wraps Server Component content as children, adding interactivity (expand, collapse, toggle) without making the content client-rendered.

  1. Why can't you import Server Components in Client Component files?

Server Components have server-only dependencies and cannot be bundled for the client. The framework prevents this to avoid leaking server code.

  1. How do you add search to a Server Component page?

Extract the search input and results into a separate Client Component. The Server Component passes initial data as props.

  1. What happens to the serialization overhead with deep nesting?

Each server-client boundary serializes props. Deep nesting adds overhead. Keep the component tree relatively flat across boundaries.

Challenge

Build a product catalog page where the product grid is a Server Component (fetches from DB), each product card is wrapped in a Client Component (handles add-to-cart click), and there is a Client Component cart sidebar. Pass the cart data via props.

Frequently Asked Questions

Can I nest Server Components inside Client Components three levels deep?

Yes. Server Components can be nested inside Client Components as children at any depth. The boundary is crossed only at the Client Component's children or props.

How do I share state between Client Components?

Use React Context in a Client Component provider, or use a state management library. State cannot live in Server Components.

Can Server Components use React Context?

Server Components cannot use React Context. Context is a client-side feature. If you need context, wrap it in a Client Component provider.

What props can I pass from Server to Client Components?

Serializable values only: strings, numbers, booleans, null, undefined, plain objects, arrays, and Date objects. Functions, class instances, and symbols are not allowed.

How do I handle themes across Server and Client Components?

Define theme variables in CSS. Server Components use the styles directly. Client Components can also use CSS variables or a ThemeProvider Client Component for dynamic switching.

Mini Project

Build a blog with a Server Component fetching the article content, a Client Component comment form, a Client Component for the share buttons, and a Client Component reading progress bar that tracks scroll position.

What's Next

Learn about Serializable Props and what can safely cross the server-client boundary.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro