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)

jsx

// This runs on the server

export default async function ProductList() {

// Direct database access - no API needed!

const products = await db.query("SELECT * FROM products");

return (

{products.map((product) => (

))}

);

}

Client Components

jsx

"use client";

import { useState } from "react";

// This runs in the browser

export default function AddToCart({ productId }) {

const [count, setCount] = useState(0);

return (

);

}

Key Benefits

1. Zero Bundle Impact

Server Components don't increase your JavaScript bundle:

jsx

// These dependencies only run on the server

import { marked } from "marked";

import { readFile } from "fs/promises";

import db from "@/lib/database";

export default async function BlogPost({ slug }) {

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

const html = marked(content);

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:

jsx

// Before: Needed an API route

export default function UserProfile({ userId }) {

const [user, setUser] = useState(null)

useEffect(() => {

fetch(/api/users/${userId})

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

.then(setUser)

}, [userId])

return user ?

{user.name}
:
Loading...

}

// After: Direct database access

export default async function UserProfile({ userId }) {

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

return

{user.name}

}

3. Automatic Code Splitting

Only interactive parts are sent to the client:

jsx

import ClientButton from "./ClientButton";

export default async function Page() {

const data = await fetchData(); // Server only

return (

{data.title}

{data.description}

{/* Only this button's code goes to client */}

);

}

Composition Patterns

Pattern 1: Server Component with Client Islands

jsx

// app/page.tsx (Server Component)

import ClientCounter from "./ClientCounter";

export default async function HomePage() {

const stats = await getServerStats();

return (

Welcome

Server-rendered stats: {stats.visitors}

{/* Interactive island */}

);

}

Pattern 2: Passing Server Components as Props

jsx

// Client Component can receive Server Components as children

'use client'

export default function Tabs({ children }) {

const [activeTab, setActiveTab] = useState(0)

return (

{children[activeTab]}

)

}

// Usage in Server Component

export default function Page() {

return (

)

}

Pattern 3: Streaming with Suspense

jsx

import { Suspense } from "react";

export default function Dashboard() {

return (

Dashboard

}> }>

);

}

async function SlowComponent() {

const data = await slowAPICall();

return

{data}
;

}

Data Fetching Best Practices

Deduplication

Next.js automatically deduplicates requests:

jsx

// This fetch is called multiple times but executed once

async function getUser(id) {

const res = await fetch(https://api.example.com/users/${id}, {

cache: "force-cache", // or 'no-store' for dynamic data

});

return res.json();

}

// Both components get cached data

export default async function Page() {

const user = await getUser(1);

return (

{/* Calls getUser(1) */} {/* Gets cached result */}

);

}

Parallel Data Fetching

jsx

export default async function Page() {

// These run in parallel

const [user, posts, comments] = await Promise.all([

fetchUser(),

fetchPosts(),

fetchComments(),

]);

return (

);

}

Sequential When Needed

jsx

export default async function Page({ params }) {

// Step 1: Get user

const user = await fetchUser(params.id);

// Step 2: Get user's team (depends on user)

const team = await fetchTeam(user.teamId);

// Step 3: Get team's projects (depends on team)

const projects = await fetchProjects(team.id);

return ;

}

Caching Strategies

Force Cache (Static)

jsx

// Cached indefinitely, revalidated on demand

const data = await fetch("https://api.example.com/data", {

cache: "force-cache",

});

No Store (Dynamic)

jsx

// Never cached, always fresh

const data = await fetch("https://api.example.com/data", {

cache: "no-store",

});

Revalidation

jsx

// Revalidate every 60 seconds

const data = await fetch("https://api.example.com/data", {

next: { revalidate: 60 },

});

Tag-based Revalidation

jsx

// Tag your data

const data = await fetch("https://api.example.com/posts", {

next: { tags: ["posts"] },

});

// Revalidate on demand

import { revalidateTag } from "next/cache";

export async function createPost(formData) {

"use server";

await savePost(formData);

revalidateTag("posts"); // Refresh all posts data

}

Common Pitfalls

❌ Don't: Use hooks in Server Components

jsx

// This will error!

export default async function Page() {

const [state, setState] = useState(0); // ❌ Error

return

{state}
;

}

✅ Do: Use Client Components for interactivity

jsx

"use client";

export default function InteractiveComponent() {

const [state, setState] = useState(0); // ✅ Works

return ;

}

❌ Don't: Pass functions as props

jsx

// Server Component

export default async function Page() {

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

return ;

}

✅ Do: Define handlers in Client Components

jsx

"use client";

export default function ClientButton() {

const handleClick = () => console.log("clicked"); // ✅ Works

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! 🚀