Skip to content
All essays
WebMarch 18, 202412 min

React Server Components Guide

A comprehensive guide to understanding and implementing React Server Components for better performance

Ü
Ümit Uz
Mobile & Full Stack Developer

React Server Components (RSC) represent a paradigm shift in how we build React applications. They allow components to run on the server while sending minimal JavaScript to the client. Let's explore this game-changing feature in depth.

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, query databases, and keep sensitive logic on the server. The rendered result is sent to the client as a special format that React efficiently turns into UI.

Key Benefits

Server Components dramatically reduce the amount of JavaScript sent to the client. They provide zero bundle size impact since they don't run in the browser. You get automatic code splitting for Server Components, and they have direct backend access without needing API layers.

Mental Model Shift

Server Components require a shift in how we think about React applications. Instead of everything running on the client, we now have three types of components: Server Components, Client Components, and Shared Components.

Server Components

Server Components are the default in Next.js 13+ and other RSC frameworks. They can't use hooks like useState or useEffect because they don't run on the client. They can't handle browser events or interact with browser APIs. But they can directly query databases and access server-side resources.

typescript
// Server Component
async function BlogPost({ id }) {
  const post = await db.post.findUnique({ where: { id } });
  return <article>{post.content}</article>;
}

Client Components

Client Components are marked with the "use client" directive. They render in the browser and can use all React features you're familiar with: state, effects, event handlers, and browser APIs.

typescript
"use client";

import { useState } from 'react';

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

Data Fetching Revolution

Server Components transform data fetching from an async operation that happens after the initial page load to something that happens during server rendering. This eliminates loading states and reduces client-side JavaScript.

No More Effect-Based Data Fetching

The old pattern of using useEffect to fetch data is gone. Server Components can directly fetch data during render, making components truly declarative.

typescript
// Old way (Client Component)
useEffect(() => {
  fetch('/api/posts')
    .then(res => res.json())
    .then(data => setPosts(data));
}, []);

// New way (Server Component)
async function PostsList() {
  const posts = await db.post.findMany();
  return posts.map(post => <Post key={post.id} post={post} />);
}

Server and Client Boundaries

Understanding where to place the boundary between server and client components is crucial for optimal RSC usage.

Default to Server Components

Start with Server Components and only mark components as "use client" when necessary. This minimizes client-side JavaScript and maximizes performance benefits.

Push Client Components Down

Keep Client Components as deep in the component tree as possible. This allows more of your app to run on the server and reduces the client JavaScript bundle.

typescript
// Server Component
export default function Page() {
  return (
    <div>
      <Header /> {/* Server Component */}
      <InteractiveWidget /> {/* Client Component */}
    </div>
  );
}

Interactivity Patterns

Server Components can't be interactive, but they can render Client Components that provide interactivity.

Composition Pattern

Pass Server Component data as props to Client Components. The Client Component receives the pre-fetched data and handles interactivity.

typescript
// Server Component
async function ProductPage({ id }) {
  const product = await db.product.findUnique({ where: { id } });
  return <AddToCartButton product={product} />;
}

// Client Component
"use client";

function AddToCartButton({ product }) {
  const [inCart, setInCart] = useState(false);
  // Interactive logic here
}

Sharing Code Between Server and Client

Some utilities and components need to work in both server and client environments. React provides patterns for sharing code effectively.

Server-Only Utilities

Mark server-specific code with "use server" directive. This ensures the code only runs on the server and isn't included in client bundles.

typescript
"use server";

import { db } from '@/lib/db';

export async function getPosts() {
  return await db.post.findMany();
}

Universal Components

Create components that work in both contexts by avoiding browser-specific APIs and client-only features. These components can be imported by both Server and Client Components.

Streaming and Suspense

Server Components work seamlessly with React Suspense to provide progressive page loads and better perceived performance.

Suspense Boundaries

Use Suspense to show fallback UI while Server Components load. This creates a smoother user experience as parts of the page become available incrementally.

typescript
export default function Page() {
  return (
    <Suspense fallback={<Skeleton />}>
      <SlowComponent />
    </Suspense>
  );
}

Caching Strategies

Server Components introduce new caching considerations. React provides built-in caching for fetch requests, and frameworks like Next.js extend this with their own caching mechanisms.

Default Cache Behavior

fetch requests in Server Components are cached by default. This prevents redundant database queries and improves performance. Use cache: 'no-store' to bypass caching for fresh data.

typescript
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store' // Disable caching
});

Error Handling

Error handling in Server Components follows the same patterns as Client Components but with server-specific considerations.

Error Boundaries

Use Error Boundaries to catch errors in Server Components and show fallback UI. This prevents errors from breaking the entire page.

Migration Strategies

Migrating existing React applications to Server Components should be done incrementally. Start with new features and gradually migrate existing components.

Incremental Adoption

You don't have to rewrite everything at once. Use Client Components for existing code and adopt Server Components for new features. Over time, you can migrate more components to run on the server.

Best Practices

  1. 1Minimize Client Components by defaulting to Server Components
  2. 2Keep Client Components small and focused on interactivity
  3. 3Pass serializable props from Server to Client Components
  4. 4Use Server Actions for mutations
  5. 5Implement proper error boundaries
  6. 6Leverage caching strategies
  7. 7Use Suspense for progressive loading

Common Pitfalls

Avoid passing functions from Server to Client Components—functions can't be serialized. Don't use browser APIs in Server Components. Be careful about passing non-serializable data across the boundary. Remember that Server Components re-render on the server, not the client.

Conclusion

React Server Components represent the future of React development. By embracing them, you build applications that send less JavaScript to the client, have faster initial loads, and provide better user experiences. The mental model shift takes time, but the performance benefits are worth it.

Start by understanding the distinction between Server and Client Components, practice placing boundaries effectively, and gradually adopt Server Components in your applications. The future of React is server-first.

Related essays

Next essay
CSS Grid vs Flexbox Complete Guide