Skip to content
All essays
WebMarch 27, 202414 min

React Server Components Complete Guide

Master React Server Components for better performance and developer experience.

Ü
Ümit Uz
Mobile & Full Stack Developer

React Server Components (RSC) represent a paradigm shift in how we build React applications. They run only on the server, reducing bundle size and improving performance.

What Are Server Components?

Server Components are a new type of React component that render exclusively on the server. They can directly access backend resources without API layers.

javascript
// Server Component (runs on server)
// Note the .server.js suffix
async function BlogPost({ id }) {
  const post = await db.post.findUnique({ where: { id } });
  const comments = await db.comment.findMany({ where: { postId: id } });

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <CommentsList comments={comments} />
    </article>
  );
}

// Client Component (runs in browser)
'use client';
import { useState } from 'react';

function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(l => l + 1)}>{likes}</button>;
}

Key Benefits

Zero Bundle Size

Server Components don't add to your JavaScript bundle.

javascript
// This entire component adds 0 bytes to your client bundle
async function ProductsList() {
  const products = await fetch('https://api.example.com/products').then(r => r.json());

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

Direct Backend Access

No need for API endpoints.

javascript
// Before: Client Component + API
// Client component
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, [userId]);

  return user ? <Profile user={user} /> : <Loading />;
}

// API route
export async function GET(request, { params }) {
  const user = await db.user.findUnique({ where: { id: params.userId } });
  return Response.json(user);
}

// After: Server Component
async function UserProfile({ userId }) {
  const user = await db.user.findUnique({ where: { id: userId } });
  return <Profile user={user} />;
}

Automatic Code Splitting

Load only what you need.

javascript
import { Suspense } from 'react';

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsSection />
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <ChartsSection />
      </Suspense>
      <Suspense fallback={<RecentSkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

Server vs Client Components

Server Components

  • Run only on server
  • Can access databases, filesystem, internal APIs
  • Have no interactivity (no useState, useEffect)
  • Reduce client bundle size
javascript
// ✅ Server Component
async function ProductsPage() {
  const products = await db.product.findMany();

  return (
    <div>
      <h1>Products</h1>
      {products.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.description}</p>
        </div>
      ))}
    </div>
  );
}

Client Components

  • Run in browser
  • Have full React features (hooks, event handlers)
  • Can use browser APIs
  • Add to bundle size
javascript
// ✅ Client Component
'use client';
import { useState } from 'react';

function AddToCart({ productId }) {
  const [adding, setAdding] = useState(false);

  const handleAdd = async () => {
    setAdding(true);
    await fetch('/api/cart/add', {
      method: 'POST',
      body: JSON.stringify({ productId }),
    });
    setAdding(false);
  };

  return (
    <button onClick={handleAdd} disabled={adding}>
      {adding ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

Composition Patterns

Server Components Containing Client Components

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

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} />
    </div>
  );
}

// Client Component
'use client';
function AddToCart({ productId }) {
  // Interactive logic here
}

Client Components Containing Server Components

Not directly possible. Server Components must be passed as children.

javascript
// ❌ Won't work
'use client';
function Layout({ productId }) {
  return <ProductDetails id={productId} />;
}

// ✅ Works - pass as children
'use client';
function Layout({ children }) {
  return <div className="layout">{children}</div>;
}

// Usage
<Layout>
  <ProductDetails id={productId} />
</Layout>

Data Fetching Patterns

Sequential Data Fetching

javascript
async function ProductPage({ id }) {
  // Wait for product
  const product = await db.product.findUnique({ where: { id } });

  // Then wait for reviews (uses product data)
  const reviews = await db.review.findMany({
    where: { productId: product.id }
  });

  return <ProductDetails product={product} reviews={reviews} />;
}

Parallel Data Fetching

javascript
import { Suspense } from 'react';

async function ProductPage({ id }) {
  return (
    <div>
      <h1>Product Details</h1>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={id} />
      </Suspense>
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts productId={id} />
      </Suspense>
    </div>
  );
}

async function Reviews({ productId }) {
  const reviews = await db.review.findMany({
    where: { productId }
  });
  return <ReviewsList reviews={reviews} />;
}

Prefetching

javascript
'use client';
import { useRouter } from 'next/navigation';

function ProductLink({ productId, children }) {
  const router = useRouter();

  const handleMouseEnter = () => {
    router.prefetch(`/products/${productId}`);
  };

  return (
    <Link
      href={`/products/${productId}`}
      onMouseEnter={handleMouseEnter}
    >
      {children}
    </Link>
  );
}

Streaming with Suspense

Progressive Rendering

javascript
import { Suspense } from 'react';

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsLoading />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<ChartsLoading />}>
        <Charts />
      </Suspense>
      <Suspense fallback={<ActivityLoading />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

async function Stats() {
  const stats = await fetch('/api/stats').then(r => r.json());
  return <StatsDisplay stats={stats} />;
}

Loading States

javascript
// loading.js (Next.js)
export default function Loading() {
  return (
    <div className="loading">
      <Spinner />
      <p>Loading...</p>
    </div>
  );
}

// error.js
'use client';
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Server Actions

Defining Actions

javascript
// actions.ts
'use server';

export async function addTodo(formData) {
  const title = formData.get('title');
  await db.todo.create({ data: { title } });
  revalidatePath('/todos');
}

export async function deleteTodo(id) {
  await db.todo.delete({ where: { id } });
  revalidatePath('/todos');
}

Using Actions

javascript
import { addTodo } from './actions';

function TodoForm() {
  return (
    <form action={addTodo}>
      <input name="title" type="text" required />
      <button type="submit">Add Todo</button>
    </form>
  );
}

With Loading States

javascript
'use client';
import { useFormStatus } from 'react-dom';
import { addTodo } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Adding...' : 'Add Todo'}
    </button>
  );
}

function TodoForm() {
  return (
    <form action={addTodo}>
      <input name="title" type="text" required />
      <SubmitButton />
    </form>
  );
}

Migration Strategy

Gradual Migration

javascript
// Step 1: Keep existing Client Components
'use client';
function OldProductList() {
  const [products, setProducts] = useState([]);
  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts);
  }, []);
  return <div>{/* ... */}</div>;
}

// Step 2: Create new Server Component
async function NewProductList() {
  const products = await db.product.findMany();
  return <div>{/* ... */}</div>;
}

// Step 3: Use where appropriate
function ProductsPage() {
  return (
    <>
      <OldProductList /> {/* Keep for interactivity */}
      <NewProductList /> {/* Use for static content */}
    </>
  );
}

Best Practices

DOs

  • Use Server Components by default
  • Use Client Components only when needed
  • Keep Client Components small and focused
  • Server Component → Client Component (not vice versa)
  • Use Suspense for loading states
  • Optimize database queries

DON'Ts

  • Put interactive logic in Server Components
  • Use browser APIs in Server Components
  • Overuse Client Components
  • Forget to mark Client Components with 'use client'
  • Ignore loading states
  • Necessarily convert everything to Server Components

Performance Tips

Reduce Client Bundle

javascript
// ❌ Large client bundle
'use client';
import { Chart, Calendar, Map, Editor } from 'big-library';

function Dashboard() {
  return (
    <div>
      <Chart />
      <Calendar />
      <Map />
      <Editor />
    </div>
  );
}

// ✅ Server Components (0 bundle cost)
import { Chart } from 'chart-library/server';
import { Calendar } from 'calendar-library/server';
import { Map } from 'map-library/server';
import { Editor } from 'editor-library/server';

function Dashboard() {
  return (
    <div>
      <Chart />
      <Calendar />
      <Map />
      <Editor />
    </div>
  );
}

Optimize Data Fetching

javascript
// ❌ Sequential requests
async function Page() {
  const user = await fetchUser();
  const posts = await fetchPosts();  // Waits for user
  const comments = await fetchComments();  // Waits for posts
  return <Page user={user} posts={posts} comments={comments} />;
}

// ✅ Parallel requests
async function Page() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments(),
  ]);
  return <Page user={user} posts={posts} comments={comments} />;
}

Conclusion

React Server Components enable better performance and developer experience by moving more logic to the server. Use them by default, and reach for Client Components when you need interactivity. The future of React is server-first!

Enjoyed this essay? Share it.

Related essays

Next essay
Micro-Frontends Architecture Guide