Leep.
HomeBusiness DevelopmentWeb & EngineeringWork
Contact
Log in
Leep.

Custom websites and web applications for ambitious companies.

Navigation

  • Home
  • Work
  • Contact

Services

  • Website
  • App
  • AI Implementation

Resources

  • Articles
  • Dictionary

Contact

  • hello@leep.no

© 2026 Leep. All rights reserved.

Back to articles
How-To Guides10 min read

Next.js & React Server Components: A Practical Guide

Understanding when to use Server vs. Client Components and why it matters for performance.

Martin Brandvoll
Martin BrandvollFounder & Lead Consultant
Published on November 28, 2025
Code on screen representing development

Next.js & React Server Components: A Practical Guide

React Server Components (RSC) represent the biggest shift in React's architecture since hooks. But most developers are still confused about when to use them, how they actually work, and what problems they solve.

This guide cuts through the abstraction to give you practical, actionable understanding of Server Components in Next.js 14+.

The Mental Model: Where Code Runs

Before RSC, all your React code ran in the browser. Components were shipped as JavaScript, downloaded by users, parsed, and executed to render the UI.

With Server Components, React can now run components on the server. The output—not the code—is sent to the browser. This is a fundamental shift in how React applications work.

Server Components:

  • Execute on the server at request time (or build time for static content)
  • Have direct access to server resources (databases, file system, environment variables)
  • Never ship their code to the browser
  • Cannot use browser APIs or React state
Client Components:
  • Execute in the browser (with server-side rendering for initial HTML)
  • Ship as JavaScript to the browser
  • Can use all browser APIs and React interactivity features
  • Are what you've been writing all along

In Next.js 13+, all components are Server Components by default. To make a component run on the client, you add "use client" at the top.

Why This Matters: The Performance Story

Let's make this concrete with an example.

The Old Way: Client Component

// Before: Everything runs in the browser
'use client';
import { marked } from 'marked';  // 65KB library
import { format } from 'date-fns'; // 75KB library

export function BlogPost({ slug }: { slug: string }) { const [post, setPost] = useState(null);

useEffect(() => { fetch(/api/posts/${slug}) .then(res => res.json()) .then(setPost); }, [slug]);

if (!post) return ;

return (

{post.title}

{format(post.date, 'MMMM d, yyyy')}
); }

What gets sent to the browser:

  • The component code
  • The marked library (65KB)
  • The date-fns library (75KB)
  • Loading states and spinners
User experience:

  1. Download page shell
  2. Download JavaScript bundle (140KB+ just for this component)
  3. Parse and execute JavaScript
  4. Make API request for post data
  5. Wait for response
  6. Finally render the content

The New Way: Server Component

// After: Runs on the server, sends only HTML
import { marked } from 'marked';
import { format } from 'date-fns';
import { db } from '@/lib/db';

export async function BlogPost({ slug }: { slug: string }) { const post = await db.posts.findUnique({ where: { slug } });

return (

{post.title}

{format(post.date, 'MMMM d, yyyy')}
); }

What gets sent to the browser:

  • Pre-rendered HTML
  • Zero JavaScript for this component
User experience:

  1. Server fetches data, runs markdown parsing, formats dates
  2. Browser receives complete HTML
  3. Content is immediately visible
The marked and date-fns libraries never leave the server. The user's browser never downloads, parses, or executes them.

When to Use Server Components

Use Server Components when:

1. Fetching Data

Server Components can fetch data directly without API routes or useEffect:

// Direct database access - no API layer needed
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findUnique({
    where: { id: userId },
    include: { posts: true, followers: true }
  });

return (

{user.name}

{user.followers.length} followers

); }

Benefits:

  • No loading states for initial render
  • No waterfalls (fetch everything you need in one place)
  • Direct database access without API overhead
  • Sensitive queries never exposed to the client

2. Using Heavy Dependencies

Libraries that are only needed for rendering can stay on the server:

// These libraries never ship to the browser
import { Prism } from 'prismjs';        // Syntax highlighting
import { renderMath } from 'katex';     // Math rendering
import { processMarkdown } from 'remark'; // Markdown processing
import { generateOG } from 'satori';     // Image generation

async function Documentation({ slug }: { slug: string }) { const doc = await getDoc(slug); const highlighted = Prism.highlight(doc.code, 'typescript'); const rendered = processMarkdown(doc.content);

return

{/ rendered content /}
; }

3. Accessing Sensitive Resources

Server Components can access things that should never be exposed to browsers:

async function AdminDashboard() {
  // Environment variables without NEXT_PUBLIC_ prefix
  const apiKey = process.env.ADMIN_API_KEY;

// Server-only modules const { readFile } = await import('fs/promises'); const config = await readFile('./config.json', 'utf-8');

// Internal services const metrics = await internalMetricsService.getStats();

return ; }

4. Static Content

Content that doesn't change based on user interaction:

// Perfect for Server Components
function Footer() {
  return (
    
{/ navigation links /}

© 2025 Company Name

); }

function ProductCard({ product }: { product: Product }) { return (

{product.name}

{product.name}

{product.description}

{/ Interactive parts can be Client Components /}
); }

When to Use Client Components

Add "use client" when you need:

1. Interactivity

Anything that responds to user input:

'use client';

import { useState } from 'react';

export function Counter() { const [count, setCount] = useState(0);

return ( ); }

2. Browser APIs

Accessing browser-specific features:

'use client';

import { useEffect, useState } from 'react';

export function LocationDisplay() { const [location, setLocation] = useState(null);

useEffect(() => { navigator.geolocation.getCurrentPosition(setLocation); }, []);

if (!location) return

Getting location...

;

return (

Lat: {location.coords.latitude}, Long: {location.coords.longitude}

); }

3. React State and Effects

Components using useState, useEffect, useReducer, or custom hooks with state:

'use client';

import { useState, useEffect } from 'react';

export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { const [query, setQuery] = useState('');

useEffect(() => { const timeout = setTimeout(() => onSearch(query), 300); return () => clearTimeout(timeout); }, [query, onSearch]);

return ( setQuery(e.target.value)} placeholder="Search..." /> ); }

4. Event Handlers

Components with onClick, onChange, onSubmit, etc.:

'use client';

export function LikeButton({ postId }: { postId: string }) { const [liked, setLiked] = useState(false);

async function handleLike() { setLiked(!liked); await fetch(/api/posts/${postId}/like, { method: 'POST' }); }

return ( ); }

The Composition Pattern: Mixing Server and Client

The real power comes from composing Server and Client Components together.

Key rule: Server Components can import and render Client Components, but Client Components cannot import Server Components.

Pattern 1: Server Parent, Client Children

// app/posts/[slug]/page.tsx (Server Component)
import { db } from '@/lib/db';
import { CommentSection } from './CommentSection'; // Client Component
import { LikeButton } from './LikeButton';          // Client Component

export default async function PostPage({ params }: { params: { slug: string } }) { const post = await db.posts.findUnique({ where: { slug: params.slug } });

return (

{post.title}

{/ rendered content /}

{/ Interactive parts are Client Components /}

); }

Pattern 2: Passing Server Data to Client Components

// Server Component fetches data
async function ProductPage({ id }: { id: string }) {
  const product = await db.products.findUnique({ where: { id } });

return (

{product.name}

{/ Pass data as props to Client Component /}
); }

// Client Component receives serializable data 'use client';

function AddToCart({ productId, price, inventory }: Props) { const [quantity, setQuantity] = useState(1); // Interactive logic here }

Pattern 3: Children Slots

// Client Component can receive Server Components as children
'use client';

export function Modal({ children, isOpen, onClose }: Props) { if (!isOpen) return null;

return (

e.stopPropagation()}> {children} {/ Can be Server Component content! /}
); }

// Usage in Server Component async function Page() { return ( {/ This works! /} ); }

Common Mistakes and How to Avoid Them

Mistake 1: Making Everything a Client Component

// ❌ Don't do this
'use client';

export function ProductList() { const [products, setProducts] = useState([]);

useEffect(() => { fetch('/api/products').then(r => r.json()).then(setProducts); }, []);

return products.map(p => ); }

// ✅ Do this instead export async function ProductList() { const products = await db.products.findMany();

return products.map(p => ); }

Mistake 2: Importing Server-Only Code in Client Components

// ❌ This will error at build time
'use client';
import { db } from '@/lib/db'; // Server-only!

// ✅ Fetch data in Server Component, pass to Client async function Parent() { const data = await db.query(); return ; }

Mistake 3: Unnecessary Client Component Boundaries

// ❌ Entire component tree becomes client-side
'use client';
export function Page() {
  const [state, setState] = useState();
  return (
    
{/ Now client-side /} {/ Now client-side /} {/ Only this needs client /}
{/ Now client-side /}
); }

// ✅ Push client boundary down export function Page() { return (

{/ Server Component /} {/ Server Component /} {/ 'use client' here only /}
{/ Server Component /}
); }

Performance Implications

Bundle Size

Every library imported in a Client Component is shipped to the browser. With Server Components:

  • markdown parsing libraries (remark, marked): 0 KB shipped
  • syntax highlighting (prism, shiki): 0 KB shipped
  • date formatting (date-fns, moment): 0 KB shipped
  • PDF generation, image processing: 0 KB shipped

Time to Interactive

Server Components reduce JavaScript that must be downloaded, parsed, and executed before the page becomes interactive. For content-heavy pages, this can mean seconds faster interactivity.

Streaming and Suspense

Server Components work with React Suspense for streaming HTML:

import { Suspense } from 'react';

export default function Page() { return (

{/ Rendered immediately /}

}> {/ Streams in when ready /}

}> {/ Streams independently /}

); }

Decision Framework

When creating a new component, ask:

  1. Does it need interactivity? → Client Component
  2. Does it use browser APIs? → Client Component
  3. Does it need React state? → Client Component
  4. Otherwise → Keep it as Server Component (default)
If a component needs both server data AND interactivity, split it:
  • Server Component fetches data and renders static parts
  • Client Component handles interactive parts, receives data as props

The Bottom Line

React Server Components aren't just a performance optimization—they're a new way of thinking about where code runs in your application.

The mental shift: Start with Server Components, add "use client" only where you need interactivity.

This default-server approach means:

  • Less JavaScript shipped to browsers
  • Direct access to server resources
  • Simpler data fetching patterns
  • Better performance by default

The framework does the hard work of coordinating server and client rendering. Your job is to understand the boundary and place your "use client" directives thoughtfully.

Master this, and you'll build faster applications with less complexity.

Share this article:

About the Author

Martin Brandvoll

Martin Brandvoll

Founder & Lead Consultant

Martin brings 10+ years of experience bridging business strategy and technical implementation. He specializes in helping SMBs leverage technology for sustainable growth.

View all articles by Martin Brandvoll →

Table of Contents

Share this article

Related Articles

Speed metrics dashboard
Technology•4 min read

Website Speed: The Hidden Conversion Killer

Every second of load time costs you 7% in conversions. Here's how to fix it.

Martin Brandvoll
Martin Brandvoll
Dec 5, 2025
Read more
Website design and development planning
How-To Guides•12 min read

What Does a Website Cost in 2025?

A realistic breakdown of website costs in Norway, from simple one-pagers to complex web applications. Learn what affects the price and how to budget smartly.

Martin Brandvoll
Martin Brandvoll
Jan 15, 2025
Read more

Get insights delivered

Weekly articles on business strategy, technology, and building sustainable growth.

No spam. Unsubscribe anytime.