nextjs-server-components

Next.js Server Components

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nextjs-server-components" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-nextjs-server-components

Next.js Server Components

Master Server Components in Next.js to build high-performance applications with server-side rendering and data fetching.

Server Components Basics

In Next.js App Router, all components are Server Components by default:

// app/posts/page.tsx (Server Component by default) async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } // Cache for 1 hour }); if (!res.ok) throw new Error('Failed to fetch posts'); return res.json(); }

export default async function Posts() { const posts = await getPosts();

return ( <div> <h1>Blog Posts</h1> {posts.map((post: Post) => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> <span>{new Date(post.date).toLocaleDateString()}</span> </article> ))} </div> ); }

// Direct database access (server-only) import { db } from '@/lib/db';

export default async function Users() { const users = await db.user.findMany({ select: { id: true, name: true, email: true } });

return ( <div> {users.map(user => ( <div key={user.id}> {user.name} - {user.email} </div> ))} </div> ); }

Server vs Client Components Decision Tree

// Use Server Components when: // - Fetching data // - Accessing backend resources directly // - Keeping sensitive information on server // - Keeping large dependencies on server

// Server Component (default) export default async function ServerComp() { const data = await fetchData(); return <div>{data}</div>; }

// Use Client Components when: // - Using interactivity (onClick, onChange, etc.) // - Using state or lifecycle hooks (useState, useEffect) // - Using browser-only APIs (localStorage, window, etc.) // - Using custom hooks that depend on state/effects // - Using React Context

// Client Component 'use client'; import { useState } from 'react';

export default function ClientComp() { const [count, setCount] = useState(0);

return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> ); }

// Composition: Server Component with Client Component export default async function Page() { const data = await fetchData(); // Server-side

return ( <div> <ServerContent data={data} /> <InteractiveButton /> {/* Client Component */} </div> ); }

Server Actions for Mutations

// app/actions.ts - Server Actions 'use server';

import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string;

const post = await db.post.create({ data: { title, content } });

revalidatePath('/posts'); redirect(/posts/${post.id}); }

export async function updatePost(id: string, formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string;

await db.post.update({ where: { id }, data: { title, content } });

revalidatePath(/posts/${id}); return { success: true }; }

export async function deletePost(id: string) { await db.post.delete({ where: { id } }); revalidatePath('/posts'); }

// app/posts/new/page.tsx - Using Server Actions export default function NewPost() { return ( <form action={createPost}> <input name="title" placeholder="Title" required /> <textarea name="content" placeholder="Content" required /> <button type="submit">Create Post</button> </form> ); }

// With client-side enhancement 'use client'; import { useFormStatus, useFormState } from 'react-dom'; import { createPost } from './actions';

function SubmitButton() { const { pending } = useFormStatus();

return ( <button type="submit" disabled={pending}> {pending ? 'Creating...' : 'Create Post'} </button> ); }

export default function NewPostForm() { const [state, formAction] = useFormState(createPost, null);

return ( <form action={formAction}> <input name="title" required /> <textarea name="content" required /> {state?.error && <p className="error">{state.error}</p>} <SubmitButton /> </form> ); }

Data Fetching Patterns

// Parallel data fetching async function getUser(id: string) { const res = await fetch(/api/users/${id}); return res.json(); }

async function getUserPosts(id: string) { const res = await fetch(/api/users/${id}/posts); return res.json(); }

export default async function UserProfile({ params }: { params: { id: string } }) { // Fetch in parallel const [user, posts] = await Promise.all([ getUser(params.id), getUserPosts(params.id) ]);

return ( <div> <UserInfo user={user} /> <UserPosts posts={posts} /> </div> ); }

// Sequential data fetching (when needed) export default async function Dashboard() { // Fetch user first const user = await getUser();

// Then fetch user-specific data const settings = await getUserSettings(user.id); const notifications = await getUserNotifications(user.id);

return ( <div> <UserHeader user={user} settings={settings} /> <Notifications items={notifications} /> </div> ); }

// Waterfall optimization with Suspense import { Suspense } from 'react';

export default function Page() { return ( <div> {/* User loads first */} <Suspense fallback={<UserSkeleton />}> <User /> </Suspense>

  {/* These load in parallel */}
  &#x3C;div className="grid">
    &#x3C;Suspense fallback={&#x3C;SettingsSkeleton />}>
      &#x3C;Settings />
    &#x3C;/Suspense>
    &#x3C;Suspense fallback={&#x3C;NotificationsSkeleton />}>
      &#x3C;Notifications />
    &#x3C;/Suspense>
  &#x3C;/div>
&#x3C;/div>

); }

Streaming with Suspense

// app/dashboard/page.tsx import { Suspense } from 'react';

export default function Dashboard() { return ( <div> <h1>Dashboard</h1>

  {/* Fast component renders immediately */}
  &#x3C;QuickStats />

  {/* Slow components stream in */}
  &#x3C;Suspense fallback={&#x3C;ChartSkeleton />}>
    &#x3C;RevenueChart />
  &#x3C;/Suspense>

  &#x3C;Suspense fallback={&#x3C;TableSkeleton />}>
    &#x3C;RecentOrders />
  &#x3C;/Suspense>

  &#x3C;Suspense fallback={&#x3C;ActivitySkeleton />}>
    &#x3C;ActivityFeed />
  &#x3C;/Suspense>
&#x3C;/div>

); }

async function RevenueChart() { // Slow data fetch const data = await fetchRevenueData();

return <Chart data={data} />; }

async function RecentOrders() { const orders = await fetchOrders({ limit: 10 });

return ( <table> {orders.map(order => ( <tr key={order.id}> <td>{order.id}</td> <td>{order.total}</td> </tr> ))} </table> ); }

// Nested Suspense for granular streaming async function ActivityFeed() { return ( <div> <h2>Activity</h2> <Suspense fallback={<div>Loading comments...</div>}> <Comments /> </Suspense> <Suspense fallback={<div>Loading likes...</div>}> <Likes /> </Suspense> </div> ); }

Server Component Patterns

// Pattern 1: Data fetching in Server Component async function fetchProduct(id: string) { const product = await db.product.findUnique({ where: { id } }); return product; }

export default async function ProductPage({ params }: { params: { id: string } }) { const product = await fetchProduct(params.id);

return ( <div> <ProductDetails product={product} /> <AddToCartButton productId={product.id} /> {/* Client Component */} </div> ); }

// Pattern 2: Server Component as data provider async function ProductWithReviews({ id }: { id: string }) { const [product, reviews] = await Promise.all([ fetchProduct(id), fetchReviews(id) ]);

return ( <> <ProductInfo product={product} /> <ReviewList reviews={reviews} /> <ReviewForm productId={id} /> {/* Client Component */} </> ); }

// Pattern 3: Composing Server and Client Components export default async function Page() { const data = await fetchData();

return ( <ClientWrapper> {/* Server Component children work inside Client Components */} <ServerContent data={data} /> </ClientWrapper> ); }

// Pattern 4: Props passing from Server to Client export default async function Layout({ children }: { children: React.ReactNode }) { const user = await getUser();

return ( <div> <ClientHeader user={user} /> {/* Pass server data to client */} <main>{children}</main> </div> ); }

Client Component Patterns

// Pattern 1: Minimal Client Component 'use client'; import { useState } from 'react';

export function Counter({ initialCount = 0 }: { initialCount?: number }) { const [count, setCount] = useState(initialCount);

return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> ); }

// Pattern 2: Client Component with Server Component children 'use client'; import { useState } from 'react';

export function Tabs({ children }: { children: React.ReactNode }) { const [activeTab, setActiveTab] = useState(0);

return ( <div> <div className="tabs"> <button onClick={() => setActiveTab(0)}>Tab 1</button> <button onClick={() => setActiveTab(1)}>Tab 2</button> </div> <div>{children}</div> </div> ); }

// Usage: Server Component children work export default async function Page() { const data = await fetchData();

return ( <Tabs> <ServerContent data={data} /> </Tabs> ); }

// Pattern 3: Client wrapper with context 'use client'; import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext<{ theme: string; setTheme: (theme: string) => void; } | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState('light');

return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); }

// Pattern 4: Optimistic updates with Server Actions 'use client'; import { useOptimistic } from 'react'; import { addTodo } from './actions';

export function TodoList({ todos }: { todos: Todo[] }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( todos, (state, newTodo: string) => [ ...state, { id: Date.now(), text: newTodo, pending: true } ] );

async function formAction(formData: FormData) { const text = formData.get('text') as string; addOptimisticTodo(text); await addTodo(text); }

return ( <div> {optimisticTodos.map(todo => ( <div key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}> {todo.text} </div> ))} <form action={formAction}> <input name="text" /> <button type="submit">Add</button> </form> </div> ); }

Composition Strategies

// Strategy 1: Server Component as layout export default async function Layout({ children }: { children: React.ReactNode }) { const user = await getUser();

return ( <div> <ServerNav user={user} /> <Sidebar> <ClientSidebarContent /> {/* Interactive sidebar */} </Sidebar> <main>{children}</main> </div> ); }

// Strategy 2: Passing Server Components as props export default async function Page() { const posts = await getPosts();

return ( <ClientLayout sidebar={<ServerSidebar posts={posts} />} main={<ServerContent posts={posts} />} /> ); }

// Strategy 3: Server Component with multiple Client Components export default async function Dashboard() { const data = await fetchDashboardData();

return ( <div> <ClientHeader /> <ServerStats data={data.stats} /> <ClientChart data={data.chartData} /> <ServerTable data={data.tableData} /> <ClientFilters /> </div> ); }

// Strategy 4: Conditional Client Components export default async function Page() { const user = await getUser();

return ( <div> {user.isAdmin ? ( <AdminClientPanel /> ) : ( <UserServerContent user={user} /> )} </div> ); }

Server-Only Code

// lib/server-only-utils.ts import 'server-only'; // Ensures this file is never bundled for client

export async function getSecretData() { const apiKey = process.env.SECRET_API_KEY; // This code only runs on server const res = await fetch('https://api.example.com/secret', { headers: { Authorization: Bearer ${apiKey} } }); return res.json(); }

export function decryptData(encrypted: string) { // Encryption logic that should never reach client return decrypt(encrypted, process.env.ENCRYPTION_KEY); }

// app/dashboard/page.tsx import { getSecretData } from '@/lib/server-only-utils';

export default async function Dashboard() { const data = await getSecretData();

return <div>{data.publicInfo}</div>; }

// Type-safe environment variables // env.ts import 'server-only';

export const env = { DATABASE_URL: process.env.DATABASE_URL!, API_KEY: process.env.API_KEY!, SECRET_KEY: process.env.SECRET_KEY! } as const;

// Usage in Server Components import { env } from '@/lib/env';

export default async function Page() { const data = await fetch('https://api.example.com', { headers: { 'X-API-Key': env.API_KEY } });

return <div>Data fetched</div>; }

Performance Implications

// Heavy dependency only loaded on server import { marked } from 'marked'; // Markdown parser (large library)

export default async function MarkdownPage({ content }: { content: string }) { // This runs only on server, keeping bundle small const html = marked(content);

return <div dangerouslySetInnerHTML={{ __html: html }} />; }

// Image optimization on server import sharp from 'sharp'; // Image processing library

export default async function OptimizedImage({ src }: { src: string }) { // Process on server const buffer = await fetch(src).then(r => r.arrayBuffer()); const optimized = await sharp(buffer) .resize(800, 600) .webp() .toBuffer();

const base64 = optimized.toString('base64');

return <img src={data:image/webp;base64,${base64}} />; }

// Compute-intensive operations on server export default async function AnalyticsPage() { // Heavy computation runs on server const rawData = await fetchRawData(); const processed = processLargeDataset(rawData); // CPU-intensive const stats = calculateStatistics(processed); // More computation

return <StatsDisplay stats={stats} />; }

// Multiple data sources aggregation export default async function AggregatedDashboard() { const [analytics, sales, inventory] = await Promise.all([ fetchAnalytics(), fetchSales(), fetchInventory() ]);

const report = generateReport(analytics, sales, inventory);

return <ReportView report={report} />; }

Error Handling in Server Components

// Throwing errors in Server Components export default async function PostPage({ params }: { params: { id: string } }) { const post = await db.post.findUnique({ where: { id: params.id } });

if (!post) { throw new Error('Post not found'); }

return <Article post={post} />; }

// Using notFound() for 404s import { notFound } from 'next/navigation';

export default async function UserPage({ params }: { params: { id: string } }) { const user = await db.user.findUnique({ where: { id: params.id } });

if (!user) { notFound(); // Renders not-found.tsx }

return <UserProfile user={user} />; }

// Handling errors with try-catch export default async function Page() { try { const data = await fetchData(); return <Content data={data} />; } catch (error) { console.error('Failed to fetch data:', error); return <ErrorMessage />; } }

// Error boundaries for Server Components // Caught by nearest error.tsx export default async function RiskyComponent() { const data = await riskyOperation(); // May throw

return <Display data={data} />; }

When to Use This Skill

Use nextjs-server-components when you need to:

  • Fetch data on the server

  • Access backend resources directly

  • Keep sensitive data server-side

  • Reduce client bundle size

  • Improve initial page load

  • Build SEO-optimized pages

  • Stream content to the client

  • Implement zero-JS pages

  • Optimize for performance

  • Access databases directly

  • Use server-only libraries

  • Perform heavy computations

Best Practices

Use server components by default - Only add 'use client' when you need interactivity, state, or browser APIs.

Fetch data close to where it's used - Colocate data fetching with the components that use the data.

Leverage parallel data fetching - Use Promise.all to fetch independent data sources simultaneously.

Use streaming for better UX - Wrap slow components in Suspense to show content as it loads.

Keep sensitive logic server-side - Database queries, API keys, and business logic should stay on server.

Minimize client components - Each 'use client' boundary increases JavaScript bundle size.

Cache data appropriately - Use Next.js caching strategies (revalidate, no-store) based on data freshness needs.

Handle errors gracefully - Use error boundaries and not-found pages for better error handling.

Use TypeScript for type safety - Define proper types for props and data to catch errors early.

Test server component behavior - Verify data fetching, error handling, and rendering in tests.

Common Pitfalls

Using client-only APIs in server components - window, localStorage, document are not available on server.

Not handling async errors properly - Unhandled promise rejections crash the entire page.

Mixing server and client state - State management libraries don't work in server components.

Overusing client components - Adding 'use client' unnecessarily increases bundle size and reduces performance.

Not leveraging data streaming - Missing Suspense boundaries cause slow page loads instead of progressive rendering.

Ignoring caching strategies - Not setting revalidate times leads to stale data or unnecessary requests.

Not optimizing data fetching - Sequential fetches cause waterfalls; use parallel fetching when possible.

Exposing sensitive data - Accidentally passing secrets or API keys to client components.

Not handling loading states - Missing loading UI during data fetching creates poor user experience.

Misunderstanding component boundaries - Server components can't be imported into client components directly.

Resources

  • Next.js Server Components

  • Client Components

  • Composition Patterns

  • Server Actions

  • Data Fetching

  • React Server Components

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

atomic-design-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review