How to Use React Server Components

React Server Components let you render parts of your UI on the server before they reach the browser. This guide shows you how to use them in a Next.js App Router project, where Server Components are supported by default.

What you’ll build or solve

You’ll create a Server Component that fetches data and renders HTML without sending extra component JavaScript to the browser. Done means you can keep server-only code on the server and move interactive UI into Client Components when needed.

When this approach works best

React Server Components work best when:

  • You need to fetch data before rendering a page.
  • You want to keep database, API, or file-system logic out of the browser.
  • You want to reduce client-side JavaScript.
  • You are building a page with mostly static or read-only content.

This is a bad idea for components that need browser APIs, click handlers, local state, or effects. Use a Client Component for those parts.

Prerequisites

  • A React framework that supports Server Components, such as Next.js App Router
  • Basic React component knowledge
  • Basic knowledge of async functions and fetch
  • A running project with an app folder

React Server Components are usually used through a framework. In Next.js App Router, files like app/page.js and app/layout.js are Server Components by default.

Step-by-step instructions

Step 1: Create a Server Component

In a Next.js App Router project, create or open a page file:

app/page.js

Add a normal React component:

JavaScript

export default function HomePage() {
    return (
        <main>
            <h1>Welcome back</h1>
            <p>This page is rendered as a Server Component.</p>
        </main>
    );
}

You do not need to add a special directive to make this a Server Component. In the App Router, it is server-rendered by default.

Do not add "use client" at the top unless the component needs client-side features.

What to look for

  • No "use client" directive at the top.
  • No useState, useEffect, or browser-only APIs.
  • The component can be an async function if it needs data.
  • Server-only logic should stay in this component or in server-only helper files.

Step 2: Fetch data on the server

Server Components can fetch data before rendering.

JavaScript

async function getPosts() {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");

    if (!response.ok) {
        throw new Error("Failed to fetch posts");
    }

    return response.json();
}

export default async function HomePage() {
    const posts = await getPosts();

    return (
        <main>
            <h1>Latest posts</h1>

            <ul>
                {posts.map((post) => (
                    <li key={post.id}>{post.title}</li>
                ))}
            </ul>
        </main>
    );
}

The data is loaded on the server, then the rendered result is sent to the browser.

This keeps the fetching logic out of the client bundle. It also avoids the common pattern of rendering an empty page first and then fetching data in useEffect.

Step 3: Move interactive code into a Client Component

Server Components cannot use local state, effects, event handlers, or browser APIs. If you need interactivity, create a separate Client Component.

Create a file:

app/components/LikeButton.js

Add "use client" at the top:

JavaScript

"use client";

import { useState } from "react";

export default function LikeButton() {
    const [likes, setLikes] = useState(0);

    return (
        <button onClick={() => setLikes(likes + 1)}>
            Likes: {likes}
        </button>
    );
}

Now import it into your Server Component:

JavaScript

import LikeButton from "./components/LikeButton";

export default function HomePage() {
    return (
        <main>
            <h1>Server-rendered page</h1>
            <LikeButton />
        </main>
    );
}

This keeps the page mostly server-rendered while sending only the interactive button JavaScript to the browser.

Step 4: Pass simple data to Client Components

A Server Component can pass props to a Client Component. Keep those props serializable, such as strings, numbers, booleans, arrays, and plain objects.

Server Component:

JavaScript

import UserCard from "./components/UserCard";

export default async function HomePage() {
    const user = {
        name: "Alex",
        role: "Frontend developer"
    };

    return (
        <main>
            <UserCard user={user} />
        </main>
    );
}

Client Component:

JavaScript

"use client";

export default function UserCard({ user }) {
    return (
        <article>
            <h2>{user.name}</h2>
            <p>{user.role}</p>
        </article>
    );
}

Avoid passing functions, database connections, or class instances from Server Components to Client Components. Keep complex logic on the server and pass the final data the client needs.

Examples you can copy

Example 1: Server-rendered product list

JavaScript

async function getProducts() {
    return [
        { id: 1, name: "HTML Course" },
        { id: 2, name: "CSS Course" },
        { id: 3, name: "React Course" }
    ];
}

export default async function ProductsPage() {
    const products = await getProducts();

    return (
        <main>
            <h1>Courses</h1>

            <ul>
                {products.map((product) => (
                    <li key={product.id}>{product.name}</li>
                ))}
            </ul>
        </main>
    );
}

This page does not need client-side state, so it can stay as a Server Component.

Example 2: Server page with an interactive search box

Server Component:

JavaScript

import SearchBox from "./components/SearchBox";

export default function CoursesPage() {
    return (
        <main>
            <h1>Find a course</h1>
            <SearchBox />
        </main>
    );
}

Client Component:

JavaScript

"use client";

import { useState } from "react";

export default function SearchBox() {
    const [query, setQuery] = useState("");

    return (
        <input
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search courses"
        />
    );
}

The page stays server-rendered, while the input runs in the browser.

Example 3: Fetch profile data on the server

JavaScript

async function getProfile() {
    return {
        name: "Sam",
        completedLessons: 24
    };
}

export default async function ProfilePage() {
    const profile = await getProfile();

    return (
        <main>
            <h1>{profile.name}</h1>
            <p>Completed lessons: {profile.completedLessons}</p>
        </main>
    );
}

Use this pattern for read-only profile, dashboard, or content pages.

Common mistakes and how to fix them

Mistake 1: Using useState in a Server Component

What you might do:

JavaScript

import { useState } from "react";

export default function HomePage() {
    const [count, setCount] = useState(0);

    return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Why it breaks: Server Components render on the server. They cannot use client-side state or event handlers.

Correct approach:

JavaScript

"use client";

import { useState } from "react";

export default function Counter() {
    const [count, setCount] = useState(0);

    return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Move the interactive part into a Client Component.

Mistake 2: Adding "use client" too high in the tree

What you might do:

JavaScript

"use client";

import Header from "./Header";
import CourseList from "./CourseList";

export default function Page() {
    return (
        <>
            <Header />
            <CourseList />
        </>
    );
}

Why it breaks performance: Everything imported into this file becomes part of the client-side component tree. This can send more JavaScript to the browser than needed.

Correct approach:

JavaScript

import Header from "./Header";
import SearchBox from "./SearchBox";
import CourseList from "./CourseList";

export default function Page() {
    return (
        <>
            <Header />
            <SearchBox />
            <CourseList />
        </>
    );
}

Put "use client" only inside SearchBox if that is the only interactive part.

Mistake 3: Passing non-serializable props

What you might do:

JavaScript

import UserCard from "./components/UserCard";

export default function Page() {
    const user = new User("Alex");

    return <UserCard user={user} />;
}

Why it breaks: Client Components need props that can be sent safely from the server to the browser. Class instances and functions are not good values to pass across that boundary.

Correct approach:

JavaScript

const user = {
    name: "Alex"
};

return <UserCard user={user} />;

Pass plain data instead.

Troubleshooting

If React says hooks are not supported, move the hook into a Client Component.

If an event handler does not work, check that the component file starts with "use client".

If browser APIs like window or document are undefined, the code is running on the server.

If too much JavaScript loads in the browser, move "use client" lower in the component tree.

If data does not update as expected, check your framework’s caching and revalidation settings.

If props fail to pass into a Client Component, make sure they are serializable.

Quick recap

  • Server Components render on the server and need no "use client" directive.
  • Use them for data fetching, read-only UI, and server-only logic.
  • Move state, effects, event handlers, and browser APIs into Client Components.
  • Add "use client" only to the smallest interactive component.
  • Pass simple serializable data across the server-client boundary.
  • Use a React framework, such as Next.js App Router, to work with Server Components.