Files
gh-rafaelcalleja-claude-mar…/skills/web-frameworks/references/nextjs-server-components.md
2025-11-30 08:48:52 +08:00

10 KiB

Next.js Server Components

React Server Components (RSC) architecture, patterns, and best practices.

Core Concepts

Server Components (Default)

All components in app/ directory are Server Components by default:

// app/posts/page.tsx - Server Component
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

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

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  )
}

Benefits:

  • Fetch data on server (direct database access)
  • Keep sensitive data/keys on server
  • Reduce client-side JavaScript bundle
  • Improve initial page load and SEO
  • Cache results on server
  • Stream content to client

Limitations:

  • Cannot use React hooks (useState, useEffect, useContext)
  • Cannot use browser APIs (window, localStorage)
  • Cannot add event listeners (onClick, onChange)
  • Cannot use React class components

Client Components

Mark with 'use client' directive at top of file:

// components/counter.tsx - Client Component
'use client'

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)

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

Use Client Components for:

  • Interactive UI (event handlers)
  • State management (useState, useReducer)
  • Effects (useEffect, useLayoutEffect)
  • Browser-only APIs (localStorage, geolocation)
  • Custom React hooks
  • Context consumers

Composition Patterns

Server Component as Wrapper

Best practice: Keep Server Components as parent, pass Client Components as children:

// app/page.tsx - Server Component
import { ClientSidebar } from './sidebar'
import { ClientButton } from './button'

export default async function Page() {
  const data = await fetchData() // Server-side data fetch

  return (
    <div>
      <h1>Server-rendered heading</h1>
      <ClientSidebar />
      <ClientButton />
      <p>More server-rendered content: {data.title}</p>
    </div>
  )
}

Passing Server Components to Client Components

Use children pattern to avoid making entire tree client-side:

// app/page.tsx - Server Component
import { ClientProvider } from './client-provider'
import { ServerContent } from './server-content'

export default function Page() {
  return (
    <ClientProvider>
      <ServerContent /> {/* Stays as Server Component */}
    </ClientProvider>
  )
}

// client-provider.tsx - Client Component
'use client'

export function ClientProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState()

  return <div>{children}</div>
}

// server-content.tsx - Server Component
export async function ServerContent() {
  const data = await fetchData()
  return <p>{data.content}</p>
}

Sharing Data Between Server Components

No need for props or context - just fetch data where needed:

// lib/data.ts
export async function getUser() {
  const res = await fetch('https://api.example.com/user', {
    cache: 'force-cache' // Will dedupe automatically
  })
  return res.json()
}

// app/header.tsx
import { getUser } from '@/lib/data'

export async function Header() {
  const user = await getUser() // Fetch 1
  return <div>Welcome, {user.name}</div>
}

// app/profile.tsx
import { getUser } from '@/lib/data'

export async function Profile() {
  const user = await getUser() // Fetch 2 (deduped automatically)
  return <div>Email: {user.email}</div>
}

Next.js automatically dedupes identical fetch requests during render.

Async Components

Server Components can be async functions:

// app/posts/[id]/page.tsx
async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`)
  return res.json()
}

async function getComments(postId: string) {
  const res = await fetch(`https://api.example.com/posts/${postId}/comments`)
  return res.json()
}

export default async function Post({ params }: { params: { id: string } }) {
  // Parallel data fetching
  const [post, comments] = await Promise.all([
    getPost(params.id),
    getComments(params.id)
  ])

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <CommentsList comments={comments} />
    </article>
  )
}

Streaming with Suspense

Stream components as they resolve:

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

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  return <div>Loaded after 3 seconds</div>
}

async function FastComponent() {
  await new Promise(resolve => setTimeout(resolve, 500))
  return <div>Loaded after 0.5 seconds</div>
}

export default function Page() {
  return (
    <div>
      <h1>Instant heading</h1>

      <Suspense fallback={<div>Loading fast...</div>}>
        <FastComponent />
      </Suspense>

      <Suspense fallback={<div>Loading slow...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

Benefits:

  • Fast components render immediately
  • Slow components don't block page
  • Progressive enhancement
  • Better perceived performance

Context in Server/Client Components

Problem: Context Requires Client Components

// ❌ Won't work - Server Components can't use context
import { createContext } from 'react'

const ThemeContext = createContext()

export default function Layout({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  )
}

Solution: Create Client Component Wrapper

// app/providers.tsx - Client Component
'use client'

import { createContext, useContext } from 'react'

const ThemeContext = createContext('light')

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  return useContext(ThemeContext)
}

// app/layout.tsx - Server Component
import { ThemeProvider } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Third-Party Component Integration

Many third-party components need client-side features:

// components/carousel.tsx
'use client'

import 'slick-carousel/slick/slick.css'
import Slider from 'react-slick'

export function Carousel({ children }) {
  return <Slider>{children}</Slider>
}

// app/page.tsx - Server Component
import { Carousel } from '@/components/carousel'

export default function Page() {
  return (
    <Carousel>
      <div>Slide 1</div>
      <div>Slide 2</div>
    </Carousel>
  )
}

Server Actions

Call server-side functions from Client Components:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'

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

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

  revalidatePath('/posts')
}

// app/new-post/page.tsx
import { createPost } from '@/app/actions'

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  )
}

With Client Component:

// components/post-form.tsx
'use client'

import { createPost } from '@/app/actions'
import { useFormStatus } from 'react-dom'

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

export function PostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <SubmitButton />
    </form>
  )
}

When to Use Each Component Type

Use Server Components When:

  • Fetching data from database or API
  • Accessing backend resources directly
  • Keeping sensitive information on server (tokens, keys)
  • Reducing client-side JavaScript
  • Rendering static content
  • No interactivity needed

Use Client Components When:

  • Adding interactivity (onClick, onChange)
  • Managing state (useState, useReducer)
  • Using lifecycle effects (useEffect)
  • Using browser-only APIs (localStorage, navigator)
  • Using custom React hooks
  • Using React Context
  • Using third-party libraries requiring client features

Best Practices

  1. Default to Server Components - Only use 'use client' when needed
  2. Move Client Components to leaves - Keep them as deep as possible in tree
  3. Pass Server Components as children - Avoid turning entire trees client-side
  4. Share data via fetch - Let Next.js dedupe requests automatically
  5. Use Suspense for streaming - Improve perceived performance
  6. Separate client logic - Extract client-only code to separate files
  7. Minimize client bundle - Less JavaScript = faster page loads

Common Patterns

Protected Content

// app/dashboard/page.tsx - Server Component
import { redirect } from 'next/navigation'
import { getUser } from '@/lib/auth'

export default async function Dashboard() {
  const user = await getUser()

  if (!user) {
    redirect('/login')
  }

  return <div>Welcome, {user.name}</div>
}

Optimistic Updates

// components/like-button.tsx
'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, initialLikes }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, amount) => state + amount
  )

  return (
    <button
      onClick={async () => {
        addOptimisticLike(1)
        await likePost(postId)
      }}
    >
      Likes: {optimisticLikes}
    </button>
  )
}

Loading States with Streaming

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

async function RevenueChart() {
  const data = await fetchRevenue() // Slow query
  return <Chart data={data} />
}

async function RecentSales() {
  const sales = await fetchSales() // Fast query
  return <SalesTable sales={sales} />
}

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

      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentSales />
      </Suspense>
    </div>
  )
}