---
title: "How to migrate React to Server Components"
slug: "migrate-react-to-server-components"
category: "howto"
tags: ["howto", "react", "nextjs", "server-components", "tutorial", "frontend", "performance"]
status: "stable"
last_updated: 2026-05-14
summary: "Identify client/server boundaries, mark interactive subtrees with 'use client', move data fetching to async server components, and verify bundle size drops."
related: ["[[frontend/react-server-components]]", "[[frontend/nextjs-app-router]]", "[[frontend/react]]", "[[frontend/nextjs]]", "[[comparisons/server-vs-client-components]]", "[[glossary/server-component]]", "[[glossary/hydration]]", "[[frontend/react-performance]]"]
---

## Overview

React Server Components (RSC) render on the server and send HTML and a serialized component tree to the client, with no client-side JavaScript for the server portion. Migrating to RSC reduces bundle size, eliminates client-side data-fetching waterfalls, and simplifies authentication checks. The deep-dive rules live in [[frontend/react-server-components]]; the architectural tradeoffs are in [[comparisons/server-vs-client-components]].

## Prerequisites

- Next.js 13.4+ with the App Router enabled. RSC is available only in the App Router, not the Pages Router.
- The project builds without errors before you start. Migrate on a green baseline.
- A way to measure bundle size: `ANALYZE=true next build` with `@next/bundle-analyzer` configured.

## Steps

### 1. Audit the component tree for client dependencies

Server components cannot use browser APIs, React hooks (`useState`, `useEffect`, `useContext`), or event handlers. Start by listing what each component needs.

Create a simple audit:

```bash
# Find components using hooks or event handlers
grep -r "useState\|useEffect\|useCallback\|onClick\|onChange" src/components/ --include="*.tsx" -l
```

These files must remain client components or be split so the interactive part is isolated. Everything else is a candidate for server components.

### 2. Identify the boundary: push "use client" as deep as possible

The `"use client"` directive marks a subtree as client-rendered. Place it as deep in the tree as possible to maximize the server-rendered surface area.

Before migration (everything is a client component by default in the Pages Router):

```typescript
// components/ProductPage.tsx
"use client";
import { useState } from "react";

export function ProductPage({ productId }: { productId: string }) {
  const [qty, setQty] = useState(1);
  // ... fetch product data client-side
}
```

After migration, split at the boundary:

```typescript
// app/products/[id]/page.tsx  — SERVER component (no directive)
import { db } from "@/lib/db";
import { AddToCart } from "@/components/AddToCart";

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } });
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} price={product.price} />
    </div>
  );
}

// components/AddToCart.tsx  — CLIENT component
"use client";
import { useState } from "react";

export function AddToCart({ productId, price }: { productId: string; price: number }) {
  const [qty, setQty] = useState(1);
  // interactive logic lives here only
}
```

See [[glossary/server-component]] for the definition and constraints.

### 3. Refactor data fetching to async server components

Server components can be `async` and `await` database or API calls directly; no `useEffect` or `getServerSideProps` needed.

```typescript
// Before (client-side fetch)
"use client";
export function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
  }, [userId]);
  if (!user) return <Spinner />;
  return <div>{user.name}</div>;
}

// After (server component, async)
import { db } from "@/lib/db";
export async function UserProfile({ userId }: { userId: string }) {
  const user = await db.user.findUnique({ where: { id: userId } });
  return <div>{user.name}</div>;
}
```

Eliminate the loading spinner; use Next.js `loading.tsx` for Suspense boundaries instead. See [[frontend/nextjs-app-router]] for the file conventions.

### 4. Handle context and shared state

Server components cannot consume React context. Move shared state that crosses the server/client boundary to URL search params, cookies, or a client component wrapper that reads from context and passes props down.

```typescript
// Wrap a context consumer in a small client component
"use client";
import { useTheme } from "@/context/theme";
export function ThemedButton({ children }: { children: React.ReactNode }) {
  const { theme } = useTheme();
  return <button className={theme}>{children}</button>;
}
// The parent page that renders ThemedButton can still be a server component.
```

### 5. Measure the bundle impact

```bash
ANALYZE=true next build
```

The bundle analyzer opens in the browser. Confirm that data-fetching libraries (SWR, React Query) and server-only packages (Prisma client, `pg`) no longer appear in the client bundle. See [[frontend/react-performance]] for bundle size targets. The [[glossary/hydration]] page explains what remains in the client bundle and why.

## Verify it worked

```bash
# Build passes without errors
next build

# No "use server" imports in client bundles
ANALYZE=true next build
# Confirm Prisma, pg, axios are absent from client chunks.

# Network tab shows HTML with content (not blank RSC shell)
# Open browser DevTools > Network > filter JS; check bundle count dropped.
```

## Common errors

- **`Error: Hooks can only be called inside a function component`**. A server component imported a module that calls a hook at the module level. Move it to a client component.
- **`Cannot pass a Server Component as a prop to a Client Component`**. Server components can be passed as `children` to client components but not as arbitrary props. Restructure so the server component wraps the client component.
- **Context value is `undefined` in server components**. Context does not work in server components. Pass the value as props or move the consumer to a client component.
- **Database query runs on every render**. Next.js does not cache async server component fetches by default in App Router v14+. Use `unstable_cache` or React's `cache()` function for deduplication.
- **TypeScript reports `await` in non-async function**. Add `async` to the component function signature; server components support `async` directly.

## Related

- [[frontend/react-server-components]]
- [[frontend/nextjs-app-router]]
- [[frontend/react]]
- [[frontend/nextjs]]
- [[comparisons/server-vs-client-components]]
- [[glossary/server-component]]
- [[glossary/hydration]]
- [[frontend/react-performance]]
