April 25, 2026·8 min·By

React Server Components: What They Are and When to Use Them

ReactRSCNext.jsperformanceserver components

React Server Components (RSC) are the biggest shift in React architecture since hooks. Here's what you actually need to know.

What Are Server Components?

Server Components render on the server and send HTML (and a serialized component tree) to the client. They have no JavaScript bundle size impact, can access databases directly, and can't use state or effects.

Client Components render on the client (browser). They can use state, effects, event handlers, and browser APIs.

In Next.js 13+, all components are Server Components by default. Add 'use client' to opt into a Client Component.

The Mental Model

Server Components:
- Run on server only
- Zero bundle size (JS not sent to browser)
- Can access databases, file system, secrets
- Can't use useState, useEffect, event handlers
- Can't use browser APIs

Client Components:
- Run in browser (and optionally on server via SSR)
- Added to JS bundle
- Can use all React hooks
- Can use browser APIs (window, document, etc.)
- Receive data from server as props

Data Fetching Patterns

// Server Component -- fetch directly, no useEffect needed
// app/products/page.tsx (Server Component by default)
async function ProductsPage() {
    // This runs on the server -- DB access is fine here
    const products = await db.query('SELECT * FROM products WHERE active = true')

    return (
        <div>
            <h1>Products</h1>
            {products.map(p => (
                <ProductCard key={p.id} product={p} />
            ))}
        </div>
    )
}

// ProductCard can be a Server Component too if it doesn't need interactivity
function ProductCard({ product }: { product: Product }) {
    return (
        <div>
            <h2>{product.name}</h2>
            <p>{product.price}</p>
            <AddToCartButton productId={product.id} />  {/* Client Component */}
        </div>
    )
}
// Client Component -- interactive
'use client'
import { useState } from 'react'

function AddToCartButton({ productId }: { productId: string }) {
    const [added, setAdded] = useState(false)

    return (
        <button onClick={() => {
            addToCart(productId)
            setAdded(true)
        }}>
            {added ? 'Added!' : 'Add to Cart'}
        </button>
    )
}

Server Components Can't Import Client Components

Actually they can -- but only as leaves. The boundary flows one way:

Server Component
  -> can render -> Server Component ✓
  -> can render -> Client Component ✓

Client Component
  -> can render -> Client Component ✓
  -> can render -> Server Component ✗  (won't work)
  -> EXCEPT as children passed from Server ✓
// This works! Server component passed as children to Client
// ServerParent.tsx (Server Component)
import { Modal } from './Modal'  // Client Component

export default function Page() {
    const data = await fetchData()
    return (
        <Modal>
            <ServerContent data={data} />  {/* Stays a Server Component */}
        </Modal>
    )
}

Streaming with Suspense

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Skeleton } from '@/components/ui/skeleton'

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

            {/* This streams in as soon as RevenueChart is ready */}
            <Suspense fallback={<Skeleton className="h-48" />}>
                <RevenueChart />  {/* Slow data source */}
            </Suspense>

            {/* This streams independently */}
            <Suspense fallback={<Skeleton className="h-32" />}>
                <RecentActivity />  {/* Different data source */}
            </Suspense>
        </div>
    )
}

// Each of these fetches independently and streams when ready
async function RevenueChart() {
    const data = await fetchRevenue()  // Can take 500ms
    return <Chart data={data} />
}

async function RecentActivity() {
    const events = await fetchEvents()  // Can take 200ms
    return <ActivityList events={events} />
}

Common Pitfalls

1. Context in Server Components

// Context doesn't work in Server Components
// Bad: using createContext at the server level
// Good: put Context in a Client Component wrapper
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext('dark')

2. Event handlers in Server Components

// Will throw an error
// Server Component
function Page() {
    return <button onClick={() => alert('hi')}>Click</button>
    // ^ Error: Event handlers cannot be passed to Client Component props
}

Mark the component 'use client' or extract the button into a Client Component.

3. Date/time in Server Components

// Different output on server vs client = hydration mismatch
function ServerComponent() {
    return <p>{new Date().toLocaleString()}</p>  // Mismatch!
}

// Fix: use suppressHydrationWarning or fetch at request time

Decision Tree

Does this component use:
  - useState / useReducer? -> Client
  - useEffect / useLayoutEffect? -> Client
  - Browser APIs (window, document)? -> Client
  - Event handlers (onClick, onChange)? -> Client
  - Custom hooks that use any of the above? -> Client

Otherwise, default to Server Component.

Server Components shine for: data fetching, static content, layout, large dependencies (syntax highlighters, markdown parsers) -- keep them off the client bundle.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

When should I use Server Components vs Client Components?+

Server Components are the default — use them for data fetching, static content, and anything that doesn't need interactivity. Only switch to Client Components when you need browser APIs, state, effects, or event handlers. The less JS you ship to the browser, the faster your site.

Why is my Next.js build so slow?+

Usually it's TypeScript type-checking or a large dependency. Try `NEXT_TELEMETRY_DISABLED=1 next build` with `--profile` and look at what's taking time. Splitting large data files and enabling turborepo are the two biggest wins.

Should I use the App Router or Pages Router for a new project?+

App Router for new projects, full stop. Pages Router still works fine but receives fewer improvements. The learning curve for App Router is steep for one week, then you never want to go back.

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation