Initial commit
This commit is contained in:
495
skills/web-frameworks/references/nextjs-server-components.md
Normal file
495
skills/web-frameworks/references/nextjs-server-components.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```tsx
|
||||
// ❌ 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
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
```tsx
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user