React Server Components: Architecture and Data Fetching (2026)
React Server Components (RSC) fundamentally change where your React code runs. Instead of shipping all components as JavaScript to the browser, RSC lets you render components on the server — accessing databases, file systems and secrets directly — and stream the result to the client. This guide covers the RSC mental model, component boundary decisions, data fetching patterns and integration with Next.js App Router.
Table of Contents
What Are React Server Components
React Server Components render on the server and send a serialized component tree (not HTML, not JSON — a special RSC payload) to the client. The key properties:
- Zero bundle impact — Server Component code never ships to the browser
- Direct backend access — query databases, read files, call internal APIs without an extra HTTP round-trip
- No hooks — Server Components cannot use useState, useEffect or any hook that requires a browser runtime
- Async by default — Server Components can be async functions, awaiting data directly
Server vs Client Components
In Next.js App Router, all components are Server Components by default. Add 'use client' at the top of a file to make it a Client Component.
// ServerComponent.tsx — Server Component (default, no directive needed)
async function ProductList() {
// Direct database access — no API route needed
const products = await db.query('SELECT * FROM products LIMIT 20')
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
// ClientComponent.tsx — Client Component
'use client'
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
Use Server Components when:
- Fetching data from a database or internal service
- Accessing secrets/environment variables
- Rendering large dependencies (markdown parsers, syntax highlighters) without shipping them to clients
- No interactivity or browser APIs required
Use Client Components when:
- Using useState, useEffect, useContext or any React hook
- Handling click events, form inputs, or other user interactions
- Using browser-only APIs (localStorage, geolocation, IntersectionObserver)
- Using third-party libraries that depend on browser globals
Data Fetching in Server Components
Server Components make data fetching straightforward — async/await directly in the component body:
// app/users/page.tsx
import { db } from '@/lib/db'
async function UsersPage() {
// Runs on the server — db credentials never exposed to client
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
take: 50,
})
return (
<main>
<h1>Users</h1>
{users.map(user => (
<div key={user.id}>
<p>{user.name} — {user.email}</p>
</div>
))}
</main>
)
}
export default UsersPage
Parallel data fetching — use Promise.all to avoid waterfall:
async function DashboardPage() {
// Parallel — both fetch at the same time
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders(),
])
return <Dashboard user={user} orders={orders} />
}
Streaming with Suspense
Wrap slow Server Components in <Suspense> to stream the fast parts first and progressively render slow parts:
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { UserProfile } from './UserProfile'
import { RecentOrders } from './RecentOrders'
import { Skeleton } from '@/components/Skeleton'
export default function DashboardPage() {
return (
<div>
{/* Fast — renders immediately */}
<h1>Dashboard</h1>
{/* Slower — streams in when ready */}
<Suspense fallback={<Skeleton />}>
<UserProfile />
</Suspense>
{/* Independent — streams separately */}
<Suspense fallback={<Skeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}
Composition Patterns
Server Components can render Client Components, but Client Components cannot import Server Components (only pass them as children/props):
// ✅ Correct — pass Server Component as children
// app/layout.tsx (Server Component)
import { Sidebar } from './Sidebar' // Client Component
import { Analytics } from './Analytics' // Server Component
export default function Layout({ children }) {
return (
<Sidebar>
{/* Server Component passed as prop — works fine */}
<Analytics />
{children}
</Sidebar>
)
}
// ❌ Wrong — importing Server Component inside Client Component
'use client'
import { ServerComponent } from './ServerComponent' // Error!
RSC in Next.js App Router
Next.js App Router is built around RSC. Key file conventions:
app/
layout.tsx ← Root layout (Server Component)
page.tsx ← Route page (Server Component by default)
loading.tsx ← Suspense fallback for the route segment
error.tsx ← Error boundary ('use client' required)
not-found.tsx ← 404 boundary
The loading.tsx file automatically wraps the page in Suspense:
// app/products/loading.tsx
export default function Loading() {
return <div className="animate-pulse">Loading products...</div>
}
// app/products/page.tsx — automatically wrapped in Suspense by loading.tsx
async function ProductsPage() {
const products = await fetchProducts() // Can be slow
return <ProductList products={products} />
}
Caching and Revalidation
Next.js extends the native fetch API with caching options:
// Cache forever (static)
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache',
})
// No cache (always fresh)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
// Revalidate every 60 seconds (ISR-style)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
})
// Tag-based revalidation
const data = await fetch('https://api.example.com/products', {
next: { tags: ['products'] },
})
// Invalidate from a Server Action
import { revalidateTag } from 'next/cache'
await revalidateTag('products')
Common Pitfalls
- Serialization errors — props passed from Server to Client Components must be serializable (no functions, Dates become strings)
- Context in Server Components — React context doesn't work in Server Components; use it only in Client trees
- Waterfall fetches — fetching inside a child Server Component that's inside another awaiting Server Component creates a waterfall. Hoist fetches up or use Promise.all
- Marking too many components 'use client' — adding 'use client' too high in the tree converts subtrees unnecessarily; push it as low as possible
FAQ
Can I use React Query with Server Components?
TanStack Query works in Client Components. For Server Components, fetch directly. You can prefetch on the server and hydrate TanStack Query on the client using HydrationBoundary.
Do Server Components support cookies and headers?
Yes — use cookies() and headers() from next/headers in Server Components or Server Actions.
Can Server Components call third-party APIs?
Yes — any fetch or HTTP call works. The difference is the call happens on your server, keeping API keys secret and adding server-side caching.