React Server Components (RSC) and Client Components (RCC) fundamentally change how React apps are rendered, but the most surprising truth is that all components are Server Components by default unless explicitly marked otherwise.

Let’s watch this in action. Imagine a simple Page.tsx file in a Next.js app:

// app/Page.tsx
import { Suspense } from 'react';
import UserProfile from './UserProfile';
import LoadingSpinner from './LoadingSpinner';

async function Page() {
  const userData = await fetch('https://api.example.com/user/123').then(res => res.json());

  return (
    <div>
      <h1>Welcome, {userData.name}!</h1>
      <Suspense fallback={<LoadingSpinner />}>
        <UserProfile userId={userData.id} />
      </Suspense>
    </div>
  );
}

export default Page;

And here’s UserProfile.tsx:

// app/UserProfile.tsx
async function UserProfile({ userId }: { userId: string }) {
  const userProfile = await fetch(`https://api.example.com/users/${userId}/profile`).then(res => res.json());

  return (
    <div>
      <p>Email: {userProfile.email}</p>
      <p>Bio: {userProfile.bio}</p>
    </div>
  );
}

export default UserProfile;

In this scenario, Page.tsx and UserProfile.tsx are both Server Components. They run entirely on the server. The fetch calls happen on the server, the data is fetched, and the resulting HTML is sent to the browser. The browser never sees the JavaScript for these components; it just receives the rendered HTML. Suspense here is also a server-side primitive, allowing the server to stream HTML as data becomes available.

Now, let’s introduce a Client Component. We’ll mark UserProfile.tsx as a client component by adding 'use client'; at the top.

// app/UserProfile.tsx
'use client'; // This line marks it as a Client Component

import { useState, useEffect } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const [profileData, setProfileData] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchProfile() {
      const res = await fetch(`/api/users/${userId}/profile`); // Client-side fetch
      const data = await res.json();
      setProfileData(data);
      setLoading(false);
    }
    fetchProfile();
  }, [userId]);

  if (loading) {
    return <div>Loading profile...</div>;
  }

  return (
    <div>
      <p>Email: {profileData.email}</p>
      <p>Bio: {profileData.bio}</p>
    </div>
  );
}

export default UserProfile;

When UserProfile.tsx is marked as a Client Component, the Page.tsx component (which is still a Server Component) will render a placeholder for UserProfile. The browser will then download the JavaScript for UserProfile.tsx, and that JavaScript will perform the fetch request and render the profile data. The useState and useEffect hooks are client-side features.

The core problem RSC and RCC solve is the ballooning size of client-side JavaScript bundles. Historically, all your React code ran in the browser, even if parts of it only needed to render static UI or fetch initial data. RSC allows you to keep that code on the server, reducing the amount of JavaScript shipped to the client, leading to faster initial page loads and better performance.

Here’s the mental model:

  1. Server Components (Default): Run only on the server. They can async/await directly, fetch data, access server-side resources (like databases or file systems), and render HTML. They don’t have access to browser APIs (like window or localStorage) and don’t hydrate on the client. Their output is pure HTML.

  2. Client Components ('use client';): Run on the server during the initial render to generate HTML, but then they also ship their JavaScript to the client. On the client, they "hydrate" – they take over the static HTML and make it interactive using React’s client-side runtime. They can use hooks like useState, useEffect, useRef, and have access to browser APIs. They can also re-render and fetch data client-side after the initial load.

The key decision point is interactivity. If a component needs to use React Hooks (state, effects, event listeners) or browser APIs, it must be a Client Component. If a component is purely about rendering static UI, fetching data on the server, or passing props down to other components (including Client Components), it can and should remain a Server Component.

The most impactful aspect of RSC for developers is the ability to perform server-side data fetching directly within components without needing separate getServerSideProps or API routes for initial data loading. This co-location of data fetching logic with the UI that consumes it makes applications more modular and easier to reason about. Furthermore, Server Components can pass Server Component props (like JSX) to Client Components, effectively allowing server-rendered UI to be seamlessly integrated into client-rendered interactive sections.

When you have a Server Component that renders a Client Component, the Server Component’s output is sent as HTML. The Client Component’s JavaScript is then sent to the browser, and when it hydrates, it takes over that HTML. If the Client Component then fetches data, it’s doing so after the initial HTML has already arrived and been rendered.

The next major concept you’ll grapple with is how to manage state and data flow between Server Components and Client Components, and the implications for routing and data mutations.

Want structured learning?

Take the full React course →