typescriptadvanced

Server/Client Component Patterns

Correctly compose server and client components with data fetching, interactivity, and composition patterns.

typescript
// Pattern 1: Server Component fetches, Client Component renders interactive UI

// components/PostList.tsx (Server Component — no 'use client')
async function PostList() {
  // This runs on the server — direct DB/API access
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 },
  }).then(r => r.json());

  return (
    <div className="space-y-4">
      {posts.map((post: { id: string; title: string; body: string }) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

// components/PostCard.tsx ('use client' for interactivity)
'use client';

import { useState } from 'react';

function PostCard({ post }: { post: { id: string; title: string; body: string } }) {
  const [liked, setLiked] = useState(false);

  return (
    <div className="bg-[#111] border border-white/10 rounded-xl p-4">
      <h3 className="text-white font-bold">{post.title}</h3>
      <p className="text-gray-400 mt-1">{post.body.slice(0, 100)}...</p>
      <button
        onClick={() => setLiked(!liked)}
        className={`mt-2 text-sm ${liked ? 'text-red-400' : 'text-gray-500'}`}
      >
        {liked ? '\u2764 Liked' : '\u2661 Like'}
      </button>
    </div>
  );
}

// Pattern 2: Composition — passing Server Components as children to Client Components

// ClientShell.tsx
'use client';
function ClientShell({ children, sidebar }: { children: React.ReactNode; sidebar: React.ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = useState(true);
  return (
    <div className="flex">
      {sidebarOpen && <aside className="w-64">{sidebar}</aside>}
      <main className="flex-1">{children}</main>
      <button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
    </div>
  );
}

// Page.tsx (Server Component)
// export default async function Page() {
//   const data = await getData(); // server-only
//   return (
//     <ClientShell sidebar={<ServerSidebar />}>
//       <ServerContent data={data} />
//     </ClientShell>
//   );
// }

// Pattern 3: Server-only module guard
// lib/server-only.ts
// import 'server-only'; // Throws if imported in client component
// export async function getSecretData() { return process.env.SECRET; }

export { PostList, PostCard, ClientShell };

Sponsored

Vercel

Use Cases

  • RSC architecture planning
  • Server/client boundary design
  • Interactive data-driven pages

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.