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):

AllowedNot Allowed
Strings, numbers, booleansFunctions (except event handlers between client components)
Plain objects { id: 1, name: 'x' }Class instances
Arrays of serializable valuesDates (pass as ISO string, parse on client)
null, undefinedSets, 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

NeedUse
Fetch data from API or DBServer Component
Access secrets/env varsServer Component
Render static content (text, lists, cards)Server Component
Add to useState, useEffectClient Component
Handle user events (clicks, inputs)Client Component
Use browser APIs (window, localStorage)Client Component
Third-party libraries that use windowClient Component
Animate elementsClient 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

A React Server Component (RSC) is a component that runs only on the server — never in the browser. It can directly access databases, file systems, and APIs without an extra HTTP round-trip, and it ships zero JavaScript to the client. In Next.js App Router, all components are Server Components by default unless you add 'use client'.
Server Components run on the server, can be async, access backend resources directly, and send zero JS to the browser. Client Components run in the browser (and are also pre-rendered on the server for the initial HTML), have access to useState/useEffect/browser APIs, and ship their JavaScript to the client.
No. useState, useEffect, useRef, useContext, and all other hooks are not available in Server Components — they're client-side primitives. If you need any of these, the component must be a Client Component with 'use client' at the top.
Yes — this is the recommended pattern. Pass Server Components as children or props to Client Components: <ClientWrapper>{serverContent}</ClientWrapper>. The Server Component renders on the server, and the Client Component receives the already-rendered output as its children prop, without the Client Component needing to know it came from a Server Component.

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.

Related Components