React Server Components change where and when your components run. Once the mental model clicks — server renders once and sends HTML+data, client handles interaction — everything else follows naturally.
The Core Mental Model
Before RSC, every React component ran in the browser. The server was just an HTTP endpoint you called with fetch. RSC changes this by letting components themselves run on the server:
Without RSC:
Browser → fetch('/api/templates') → Server → JSON → Browser → React renders HTML
With RSC:
Server → component queries DB directly → renders HTML+data → Browser receives it
The RSC component skips the API round-trip entirely — it queries data at render time on the server and sends the result as rendered output, not raw JSON.
Server Component — Basic Pattern
In Next.js App Router, any file in app/ without 'use client' is a Server Component:
// app/templates/page.tsx — Server Component (default)
// No 'use client' = runs on the server
interface Template {
id: number
name: string
framework: string
price: number
description: string
}
// async is allowed in Server Components — not in Client Components
export default async function TemplatesPage() {
// Direct fetch — runs server-side, no CORS, no API key exposure
const templates = await fetch('https://api.lettstartdesign.com/templates', {
next: { revalidate: 3600 } // cache for 1 hour, rebuild after
}).then(r => r.json()) as Template[]
// Or query a database directly:
// const templates = await db.query('SELECT * FROM templates')
return (
<main className="container py-4">
<h1 className="fw-bold mb-4">Templates</h1>
<div className="row g-4">
{templates.map(t => (
<div key={t.id} className="col-md-4">
<div className="card border-0 shadow-sm h-100 p-4">
<span className="badge mb-2 text-white" style={{ background: '#fd4766', width: 'fit-content' }}>
{t.framework}
</span>
<h5 className="fw-bold">{t.name}</h5>
<p className="text-muted small flex-grow-1">{t.description}</p>
<p className="fw-bold mb-0" style={{ color: '#fd4766' }}>${t.price}</p>
</div>
</div>
))}
</div>
</main>
)
}
Zero JavaScript shipped to the browser from this component. The user receives fully-rendered HTML.
What Server Components Can and Cannot Do
// ✅ Server Component — can do all of this
async function ServerComponent() {
// Async/await at the component level
const data = await fetchSomething()
// Access environment variables directly (not exposed to browser)
const secret = process.env.API_SECRET_KEY
// Import server-only packages (fs, crypto, DB drivers)
const fs = require('fs')
// Access headers and cookies (Next.js specific)
const { cookies } = await import('next/headers')
const token = cookies().get('auth-token')
return <div>{data.name}</div>
}
// ❌ Server Component — CANNOT do any of this
async function BrokenServerComponent() {
const [count, setCount] = useState(0) // ❌ hooks don't work
useEffect(() => {}, []) // ❌ no lifecycle
document.getElementById('x') // ❌ no browser APIs
window.localStorage.getItem('key') // ❌ no window
element.addEventListener('click', fn) // ❌ no event listeners
}
Client Components — Opt-in with 'use client'
// components/TemplateFilter.tsx — Client Component
'use client' // This directive marks the component as client-side
import { useState } from 'react'
// Props must be serializable (no functions from server, no class instances)
export default function TemplateFilter({
frameworks,
activeFramework,
onSelect
}: {
frameworks: string[]
activeFramework: string
onSelect: (fw: string) => void // ✅ functions are fine as props from client parents
}) {
const [search, setSearch] = useState('')
const filtered = frameworks.filter(fw =>
fw.toLowerCase().includes(search.toLowerCase())
)
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Filter frameworks..."
className="border-2 rounded-xl px-3 py-2 mb-3 w-full outline-none focus:border-red-400"
/>
<div className="flex flex-wrap gap-2">
{filtered.map(fw => (
<button
key={fw}
onClick={() => onSelect(fw)}
className="px-4 py-1.5 rounded-full text-sm font-medium border-2 transition-all"
style={{
background: fw === activeFramework ? '#fd4766' : '#fff',
color: fw === activeFramework ? '#fff' : '#333',
borderColor: fw === activeFramework ? '#fd4766' : '#e5e7eb',
}}
>
{fw}
</button>
))}
</div>
</div>
)
}
Composing Server and Client Components
The most important pattern: keep Server Components as parents, push Client Components to the leaves:
// app/templates/page.tsx — Server Component (parent)
import TemplateFilter from '@/components/TemplateFilter' // Client Component
import TemplateGrid from '@/components/TemplateGrid' // Server Component
export default async function TemplatesPage() {
// Server does the data fetching
const templates = await fetchTemplates()
const frameworks = [...new Set(templates.map(t => t.framework))]
return (
<main className="container py-4">
<h1 className="fw-bold mb-4">Templates</h1>
{/* Client Component receives serializable props from server */}
<TemplateFilter frameworks={frameworks} />
{/* Server Component renders the grid — no JS needed */}
<TemplateGrid templates={templates} />
</main>
)
}
// components/TemplateGrid.tsx — Server Component
// No 'use client' — this is a Server Component even though it's in components/
export default function TemplateGrid({ templates }: { templates: Template[] }) {
return (
<div className="row g-4">
{templates.map(t => (
<div key={t.id} className="col-md-4">
<div className="card border-0 shadow-sm p-4">
<h5 className="fw-bold">{t.name}</h5>
<p className="text-muted">{t.description}</p>
</div>
</div>
))}
</div>
)
}
Passing Server Components as Children
When a Client Component needs to wrap Server-rendered content, pass it as children:
// components/Sidebar.tsx — Client Component (needs toggle state)
'use client'
import { useState, ReactNode } from 'react'
export default function Sidebar({ children }: { children: ReactNode }) {
const [collapsed, setCollapsed] = useState(false)
return (
<aside style={{ width: collapsed ? '60px' : '240px', transition: 'width 0.2s' }}
className="bg-white border-end h-100 p-3">
<button onClick={() => setCollapsed(c => !c)} className="btn btn-sm btn-outline-secondary mb-3">
{collapsed ? '→' : '←'}
</button>
{/* children are Server Components — already rendered on server */}
{!collapsed && children}
</aside>
)
}
// app/dashboard/layout.tsx — Server Component
import Sidebar from '@/components/Sidebar'
import NavLinks from '@/components/NavLinks' // Server Component
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="d-flex vh-100">
<Sidebar>
{/* NavLinks is a Server Component passed into a Client Component */}
<NavLinks />
</Sidebar>
<main className="flex-grow-1 overflow-auto p-4">{children}</main>
</div>
)
}
Serialization Rules — What Can Cross the Server/Client Boundary
When a Server Component passes props to a Client Component, the data must be serializable (convertible to JSON):
| Allowed | Not Allowed |
|---|---|
| Strings, numbers, booleans | Functions (except event handlers between client components) |
Plain objects { id: 1, name: 'x' } | Class instances |
| Arrays of serializable values | Dates (pass as ISO string, parse on client) |
null, undefined | Sets, Maps |
| JSX / ReactNode (as children) | Promises (unless using use() hook) |
// ✅ Valid — serializable props
<ClientCard
name="Marvel Dashboard"
price={49}
tags={['Angular', 'Bootstrap']}
publishedAt="2026-06-19" // Date as string
/>
// ❌ Invalid — non-serializable
<ClientCard
date={new Date()} // ❌ Date object
onClick={() => doThing()} // ❌ function from Server Component
instance={new MyClass()} // ❌ class instance
/>
When to Use Each
| Need | Use |
|---|---|
| Fetch data from API or DB | Server Component |
| Access secrets/env vars | Server Component |
| Render static content (text, lists, cards) | Server Component |
Add to useState, useEffect | Client Component |
| Handle user events (clicks, inputs) | Client Component |
Use browser APIs (window, localStorage) | Client Component |
Third-party libraries that use window | Client Component |
| Animate elements | Client Component |
The default position is: start with Server Component, add 'use client' only when needed. Most of your content-rendering UI (cards, tables, grids, text) doesn't need any browser interaction and is best left as a Server Component.
Frequently Asked Questions
Need a Full Bootstrap 5 Admin Dashboard?
Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.
Browse Templates →Use code FIRST30 for 30% off your first purchase.