Back to blog

React Server Components - A Deep Dive into the Future of React

7 min readBy Mustafa Akkaya
#React#Next.js#Server Components#Performance

React Server Components (RSC) represent a paradigm shift in how we build React applications. Let's explore what makes them revolutionary and how to use them effectively in your projects.

What Are React Server Components?

React Server Components are a new type of component that runs exclusively on the server. Unlike traditional React components that execute in the browser, RSCs:

- Run only on the server - Zero JavaScript sent to the client

- Access backend resources directly - No need for API routes

- Support async/await natively - Fetch data directly in components

- Improve performance - Reduced bundle size and faster page loads

- Enable better code splitting - Automatic optimization

Server vs Client Components

Understanding the difference is crucial:

Server Components (Default in Next.js App Router)

component.jsx

class="keyword">class="comment">// This runs on the server

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function ProductList() {

class="keyword">class="comment">// Direct database access - no API needed!

class="keyword">class="keyword">const products = class="keyword">class="keyword">await db.query(class="keyword">class="string">"class="keyword">SELECT * class="keyword">FROM products");

class="keyword">class="keyword">return (

{products.map((product) => (

))}

);

}

Client Components

component.jsx

class="keyword">class="string">"use client";

class="keyword">import { useState } class="keyword">from class="keyword">class="string">"react";

class="keyword">class="comment">// This runs in the browser

class="keyword">export class="keyword">default class="keyword">class="keyword">function AddToCart({ productId }) {

class="keyword">class="keyword">const [count, setCount] = useState(0);

class="keyword">class="keyword">return (

);

}

Key Benefits

1. Zero Bundle Impact

Server Components don't increase your JavaScript bundle:

component.jsx

class="keyword">class="comment">// These dependencies only run on the server

class="keyword">import { marked } class="keyword">from class="keyword">class="string">"marked";

class="keyword">import { readFile } class="keyword">from class="keyword">class="string">"fs/promises";

class="keyword">import db class="keyword">from class="keyword">class="string">"@/lib/database";

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function BlogPost({ slug }) {

class="keyword">class="keyword">const content = class="keyword">class="keyword">await readFile(class="keyword">class="string">posts/${slug}.md, class="keyword">class="string">"utf-8");

class="keyword">class="keyword">const html = marked(content);

class="keyword">class="keyword">return

;

}

The marked library and file system code never reach the browser!

2. Direct Backend Access

No more creating API routes for simple data fetching:

component.jsx

class="keyword">class="comment">// Before: Needed an API route

class="keyword">export class="keyword">default class="keyword">class="keyword">function UserProfile({ userId }) {

class="keyword">class="keyword">const [user, setUser] = useState(class="keyword">null)

useEffect(() => {

fetch(class="keyword">class="string">/api/users/${userId})

.then(res => res.json())

.then(setUser)

}, [userId])

class="keyword">class="keyword">return user ?

{user.name}
:
Loading...

}

class="keyword">class="comment">// After: Direct database access

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function UserProfile({ userId }) {

class="keyword">class="keyword">const user = class="keyword">class="keyword">await db.users.findUnique({ where: { id: userId } })

class="keyword">class="keyword">return

{user.name}

}

3. Automatic Code Splitting

Only interactive parts are sent to the client:

component.jsx

class="keyword">import ClientButton class="keyword">from class="keyword">class="string">"./ClientButton";

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Page() {

class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetchData(); class="keyword">class="comment">// Server only

class="keyword">class="keyword">return (

{data.title}

{data.description}

{class="keyword">class="comment">/* Only class="keyword">this button's code goes to client */}

);

}

Composition Patterns

Pattern 1: Server Component with Client Islands

component.jsx

class="keyword">class="comment">// app/page.tsx(Server Component)

class="keyword">import ClientCounter class="keyword">from class="keyword">class="string">"./ClientCounter";

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function HomePage() {

class="keyword">class="keyword">const stats = class="keyword">class="keyword">await getServerStats();

class="keyword">class="keyword">return (

Welcome

Server-rendered stats: {stats.visitors}

{class="keyword">class="comment">/* Interactive island */}

);

}

Pattern 2: Passing Server Components as Props

component.jsx

class="keyword">class="comment">// Client Component can receive Server Components as children

class="keyword">class="string">'use client'

class="keyword">export class="keyword">default class="keyword">class="keyword">function Tabs({ children }) {

class="keyword">class="keyword">const [activeTab, setActiveTab] = useState(0)

class="keyword">class="keyword">return (

{children[activeTab]}

)

}

class="keyword">class="comment">// Usage in Server Component

class="keyword">export class="keyword">default class="keyword">class="keyword">function Page() {

class="keyword">class="keyword">return (

0} /> 1} />

)

}

Pattern 3: Streaming with Suspense

component.jsx

class="keyword">import { Suspense } class="keyword">from class="keyword">class="string">"react";

class="keyword">export class="keyword">default class="keyword">class="keyword">function Dashboard() {

class="keyword">class="keyword">return (

Dashboard

}> }>

);

}

class="keyword">class="keyword">async class="keyword">class="keyword">function SlowComponent() {

class="keyword">class="keyword">const data = class="keyword">class="keyword">await slowAPICall();

class="keyword">class="keyword">return

{data}
;

}

Data Fetching Best Practices

Deduplication

Next.js automatically deduplicates requests:

component.jsx

class="keyword">class="comment">// This fetch is called multiple times but executed once

class="keyword">class="keyword">async class="keyword">class="keyword">function getUser(id) {

class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">https:class="keyword">class="comment">//api.example.com/users/${id}, {

cache: class="keyword">class="string">"force-cache", class="keyword">class="comment">// or class="keyword">class="string">'no-store' class="keyword">class="keyword">for dynamic data

});

class="keyword">class="keyword">return res.json();

}

class="keyword">class="comment">// Both components get cached data

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Page() {

class="keyword">class="keyword">const user = class="keyword">class="keyword">await getUser(1);

class="keyword">class="keyword">return (

1} /> {class="keyword">class="comment">/* Calls getUser(1) */} 1} /> {class="keyword">class="comment">/* Gets cached result */}

);

}

Parallel Data Fetching

component.jsx

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Page() {

class="keyword">class="comment">// These run in parallel

class="keyword">class="keyword">const [user, posts, comments] = class="keyword">class="keyword">await Promise.all([

fetchUser(),

fetchPosts(),

fetchComments(),

]);

class="keyword">class="keyword">return (

);

}

Sequential When Needed

component.jsx

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Page({ params }) {

class="keyword">class="comment">// Step 1: Get user

class="keyword">class="keyword">const user = class="keyword">class="keyword">await fetchUser(params.id);

class="keyword">class="comment">// Step 2: Get user's team(depends on user)

class="keyword">class="keyword">const team = class="keyword">class="keyword">await fetchTeam(user.teamId);

class="keyword">class="comment">// Step 3: Get team's projects(depends on team)

class="keyword">class="keyword">const projects = class="keyword">class="keyword">await fetchProjects(team.id);

class="keyword">class="keyword">return ;

}

Caching Strategies

Force Cache (Static)

component.jsx

class="keyword">class="comment">// Cached indefinitely, revalidated on demand

class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetch(class="keyword">class="string">"https:class="keyword">class="comment">//api.example.com/data", {

cache: class="keyword">class="string">"force-cache",

});

No Store (Dynamic)

component.jsx

class="keyword">class="comment">// Never cached, always fresh

class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetch(class="keyword">class="string">"https:class="keyword">class="comment">//api.example.com/data", {

cache: class="keyword">class="string">"no-store",

});

Revalidation

component.jsx

class="keyword">class="comment">// Revalidate every 60 seconds

class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetch(class="keyword">class="string">"https:class="keyword">class="comment">//api.example.com/data", {

next: { revalidate: 60 },

});

Tag-based Revalidation

component.jsx

class="keyword">class="comment">// Tag your data

class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetch(class="keyword">class="string">"https:class="keyword">class="comment">//api.example.com/posts", {

next: { tags: [class="keyword">class="string">"posts"] },

});

class="keyword">class="comment">// Revalidate on demand

class="keyword">import { revalidateTag } class="keyword">from class="keyword">class="string">"next/cache";

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function createPost(formData) {

class="keyword">class="string">"use server";

class="keyword">class="keyword">await savePost(formData);

revalidateTag(class="keyword">class="string">"posts"); class="keyword">class="comment">// Refresh all posts data

}

Common Pitfalls

❌ Don't: Use hooks in Server Components

component.jsx

class="keyword">class="comment">// This will error!

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Page() {

class="keyword">class="keyword">const [state, setState] = useState(0); class="keyword">class="comment">// ❌ Error

class="keyword">class="keyword">return

{state}
;

}

✅ Do: Use Client Components for interactivity

component.jsx

class="keyword">class="string">"use client";

class="keyword">export class="keyword">default class="keyword">class="keyword">function InteractiveComponent() {

class="keyword">class="keyword">const [state, setState] = useState(0); class="keyword">class="comment">// ✅ Works

class="keyword">class="keyword">return ;

}

❌ Don't: Pass functions as props

component.jsx

class="keyword">class="comment">// Server Component

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Page() {

class="keyword">class="keyword">const handleClick = () => console.log(class="keyword">class="string">"clicked"); class="keyword">class="comment">// ❌ Can't serialize

class="keyword">class="keyword">return ;

}

✅ Do: Define handlers in Client Components

component.jsx

class="keyword">class="string">"use client";

class="keyword">export class="keyword">default class="keyword">class="keyword">function ClientButton() {

class="keyword">class="keyword">const handleClick = () => console.log(class="keyword">class="string">"clicked"); class="keyword">class="comment">// ✅ Works

class="keyword">class="keyword">return ;

}

Performance Impact

Real-world improvements with RSC:

- 30-50% smaller bundle sizes - Less JavaScript to download

- Faster Time to Interactive (TTI) - Less code to parse and execute

- Better SEO - Fully rendered HTML from the server

- Reduced API calls - Direct backend access

- Improved Core Web Vitals - Better LCP, FID, and CLS scores

When to Use Each Type

Use Server Components for:

- Data fetching

- Backend resource access

- Sensitive operations (API keys, tokens)

- Large dependencies

- SEO-critical content

Use Client Components for:

- User interactions (clicks, inputs)

- Browser APIs (localStorage, geolocation)

- State management (useState, useReducer)

- Effects (useEffect)

- Event listeners

Conclusion

React Server Components are not just a feature - they're a fundamental shift in how we architect React applications. By moving non-interactive logic to the server, we create faster, more efficient applications with better user experiences.

Start by making everything a Server Component by default, then add 'use client' only when you need interactivity. This approach ensures optimal performance and developer experience.

The future of React is here, and it's running on the server! 🚀