6 min read By Alex Johnson

React Server Components Explained

Deep dive into React Server Components. Understand the architecture, benefits, and how to use them in your Next.js applications.

React Server Components JavaScript Web Development
React Server Components Explained

What are React Server Components?

React Server Components (RSC) are a new type of component that runs exclusively on the server, enabling better performance and simpler data fetching patterns.

Key Benefits

Zero Bundle Size

Server Components don’t add to your JavaScript bundle:

// ServerComponent.js - Runs only on server
import fs from 'fs';

export default async function ServerComponent() {
  // This code never reaches the client
  const data = await fs.promises.readFile('./data.json', 'utf-8');
  const parsed = JSON.parse(data);
  
  return (
    <div>
      {parsed.map(item => (
        <Card key={item.id} data={item} />
      ))}
    </div>
  );
}

Direct Backend Access

Access databases and APIs directly without creating API routes:

// app/posts/page.jsx
import { db } from '@/lib/database';

export default async function PostsPage() {
  // Direct database access
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  });
  
  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

Automatic Code Splitting

Each Server Component is automatically split:

// Only the client components are sent to browser
export default async function Page() {
  const data = await fetchData();
  
  return (
    <div>
      <ServerHeader data={data} />
      <ClientInteractive /> {/* Only this is in JS bundle */}
      <ServerFooter />
    </div>
  );
}

Server vs Client Components

Server Components

// app/components/ServerComponent.jsx
// Default in Next.js 13+ App Router

export default async function ServerComponent() {
  const data = await fetch('https://api.example.com/data');
  
  return <div>{/* Server-rendered content */}</div>;
}

Features:

  • Can be async
  • Direct backend access
  • Zero bundle size
  • Cannot use hooks
  • Cannot use browser APIs
  • Cannot handle interactivity

Client Components

// app/components/ClientComponent.jsx
'use client'; // Opt-in to client rendering

import { useState, useEffect } from 'react';

export default function ClientComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Features:

  • Can use hooks
  • Can handle interactivity
  • Can use browser APIs
  • Adds to bundle size
  • Cannot be async
  • Cannot access backend directly

Composition Patterns

Server Components with Client Children

// ServerLayout.jsx
import ClientSidebar from './ClientSidebar';

export default async function ServerLayout({ children }) {
  const user = await getUser();
  
  return (
    <div>
      <ClientSidebar user={user} />
      <main>{children}</main>
    </div>
  );
}

Passing Server Components to Client Components

// ClientWrapper.jsx
'use client';

export default function ClientWrapper({ children }) {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}
    </div>
  );
}

// Page.jsx (Server Component)
export default async function Page() {
  return (
    <ClientWrapper>
      <ServerContent /> {/* Rendered on server */}
    </ClientWrapper>
  );
}

Data Fetching Patterns

Parallel Data Fetching

export default async function Page() {
  // These fetch in parallel
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();
  const commentsPromise = fetchComments();
  
  const [user, posts, comments] = await Promise.all([
    userPromise,
    postsPromise,
    commentsPromise
  ]);
  
  return <Dashboard user={user} posts={posts} comments={comments} />;
}

Streaming with Suspense

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
      
      <Suspense fallback={<ContentSkeleton />}>
        <Content />
      </Suspense>
      
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
    </div>
  );
}

async function Header() {
  const data = await fetchHeaderData();
  return <header>{data.title}</header>;
}

Sequential Data Fetching

export default async function Page({ params }) {
  // Fetch user first
  const user = await fetchUser(params.id);
  
  // Then fetch their posts (depends on user data)
  const posts = await fetchUserPosts(user.id);
  
  return <Profile user={user} posts={posts} />;
}

Caching Strategies

Request Memoization

// This function is automatically memoized per request
async function getUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}

export default async function Page() {
  // Both calls use the same cached result
  const user1 = await getUser('123');
  const user2 = await getUser('123'); // Cached!
  
  return <div>{user1.name}</div>;
}

Data Cache

// Cache for 1 hour
export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }
  });
  
  return <div>{data.title}</div>;
}

Revalidation

// app/actions.js
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function createPost(formData) {
  // Create post
  await db.post.create({ data: formData });
  
  // Revalidate the posts page
  revalidatePath('/posts');
  
  // Or revalidate by tag
  revalidateTag('posts');
}

Best Practices

1. Keep Client Components Small

// Good - Minimal client component
'use client';

export function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}

// Use in Server Component
export default async function Post({ id }) {
  const post = await getPost(id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <LikeButton postId={id} />
    </article>
  );
}

2. Prefer Server Components

Use Server Components by default, opt into Client Components only when needed:

  • User interactions (onClick, onChange)
  • Browser APIs (localStorage, geolocation)
  • React hooks (useState, useEffect)
  • Custom hooks
  • Class components

3. Use Suspense Boundaries

export default function Page() {
  return (
    <>
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
      
      <FastComponent /> {/* Shows immediately */}
    </>
  );
}

4. Optimize Images

import Image from 'next/image';

export default async function Gallery() {
  const images = await getImages();
  
  return (
    <div>
      {images.map(img => (
        <Image
          key={img.id}
          src={img.url}
          alt={img.alt}
          width={800}
          height={600}
          loading="lazy"
        />
      ))}
    </div>
  );
}

Migration Tips

From Pages Router to App Router

// Before (Pages Router)
export async function getServerSideProps() {
  const data = await fetchData();
  return { props: { data } };
}

export default function Page({ data }) {
  return <div>{data.title}</div>;
}

// After (App Router)
export default async function Page() {
  const data = await fetchData();
  return <div>{data.title}</div>;
}

Converting Client Components

// Identify what needs to be client-side
'use client';

import { useState } from 'react';

// Keep interactivity minimal
export function InteractiveSection() {
  const [tab, setTab] = useState('overview');
  
  return (
    <div>
      <Tabs value={tab} onChange={setTab} />
      {tab === 'overview' && <Overview />}
      {tab === 'details' && <Details />}
    </div>
  );
}

Common Patterns

Loading States

export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <DataComponent />
    </Suspense>
  );
}

Error Boundaries

// app/error.jsx
'use client';

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

Conclusion

React Server Components represent a fundamental shift in how we build React applications. By running components on the server, we can achieve better performance, simpler data fetching, and smaller bundle sizes. While there’s a learning curve, the benefits are substantial for most applications.

Start by building new features with Server Components, gradually migrate existing code, and always measure the impact on your application’s performance.

A

Alex Johnson

Published on March 8, 2024

Share: