nextjs-server-client-components

Server Components in Next.JS

  • 6 min

If you come from working with Vite or Create React App, you have a very clear mental model: All your React code is downloaded into the user’s browser and executed there.

The browser downloads a giant JS file (bundle), processes it, and React starts mounting the DOM and executing useEffect.

In Next.js, the rules of the game change quite a bit. With the arrival of the App Router, Next.js adopted the React Server Components (RSC).

Now, React can be “isomorphic” or “hybrid” (what fancy words). That is, some components run on your server and never travel to the browser, while others behave as they always did.

Understanding this distinction is one of the keys to working well with Next.js (and one of the things that will confuse you the most at first… and well, also at the end 😊).

Everything is a Server Component by Default

This is rule number one: In the app folder, all components are Server Components by default.

You don’t need to do anything special. If you create a page.tsx file, that code will run on the server during the build (or on each request) and what will be sent to the browser will be pure HTML.

Advantages of Server Components

  1. Zero JavaScript to the Client: If you import a heavy library (like one for formatting dates or processing Markdown) in a Server Component, that library runs on the server and is not added to the bundle that the user downloads.
  2. Security and Backend Access: You can access the database or use secret API keys directly inside the component. Since the code never reaches the browser, it is secure.
  3. Latency: Being on the server, the component is physically close to your data.

Let’s see an example,

// src/app/page.tsx
// This is a Server Component.
// This console.log will be seen in the TERMINAL of your computer/server,
// NOT in the browser console.
import db from '@/lib/db'; 

export default async function Page() {
  console.log("Rendering on server...");
  
  // We can access the database directly
  const data = await db.query('SELECT * FROM users');

  return (
    <main>
      <h1>User List</h1>
      {/* ... */}
    </main>
  );
}

Copied!

The "use client" directive

Obviously, static HTML is fine, but React became famous for interactivity. How do we make a button that opens a menu on click? How do we use useState?

Server Components have strict limitations:

  • ❌ They cannot use Hooks (useState, useEffect, useReducer…).
  • ❌ They cannot listen to browser events (onClick, onChange, onSubmit).
  • ❌ They cannot access browser APIs (window, localStorage).

When we need interactivity, we must explicitly opt to convert the component into a Client Component. To do this, we write the "use client" directive on the first line of the file.

// src/components/Counter.tsx
'use client' // 👈 Mark this file as a Client Component

import { useState } from 'react';

export default function Counter() {
  // Now we can use hooks and events
  const [count, setCount] = useState(0);

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

Copied!

A “Client Component” does not mean it is only rendered on the client. Next.js also pre-renders it on the server (SSR) to send the initial HTML, and then hydrates it in the browser to make it interactive. It’s the “classic” React behavior.

The Boundary

The challenge with Next.js is deciding where to draw the dividing line between Server and Client.

Imagine your component tree. The root (layout.tsx) is a Server Component. You go down the branches. The moment you import a component with 'use client', you cross the boundary.

Once you cross to the client side, all child components imported within that file will also be considered client-side (automatically, without needing to add the directive).

The “Leaves of the Tree” Strategy

To maintain maximum performance, we want most of our app to be Server Components (static and lightweight) and delay the use of 'use client' as much as possible, pushing it to the leaves (the extremities) of the tree.

Making the entire layout.tsx a 'use client' because you need a login button in the Navbar. This would force the entire application’s JS to be downloaded.

Keep the layout.tsx as a Server Component, and isolate the button in a small <LoginButton /> component that has 'use client', and import it in the layout.

The Composition Problem

There is one more restriction that will give you a headache at some point (but it makes sense).

You cannot import a Server Component inside a Client Component

That is, this,

'use client'
// ❌ ERROR: This would break because ServerComponent has server code (e.g., db call)
// and the Client Component tries to include it in the browser bundle.
import ServerComponent from './ServerComponent';

export default function ClientWrapper() {
  return (
    <div>
      <ServerComponent />
    </div>
  );
}

Copied!

The solution is to pass the Server Component as children (prop).

If we use the composition pattern we saw in the “Children and Slots” article, we can do this:

// src/app/page.tsx (SERVER)
import ClientWrapper from './ClientWrapper';
import ServerComponent from './ServerComponent';

export default function Page() {
  return (
    // ✅ CORRECT: The ClientWrapper receives the already rendered ServerComponent as HTML.
    // It doesn't need to import its source code, just paint the children "hole".
    <ClientWrapper>
      <ServerComponent />
    </ClientWrapper>
  );
}

Copied!

When to use which?

Here is the reference table you should keep in mind:

FeatureServer Component (Default)Client Component ('use client')
Database Access✅ YES❌ NO (Insecure)
Confidential Data (Keys)✅ YES❌ NO (Exposed)
Hooks (useState, useEffect)❌ NO✅ YES
Events (onClick, onChange)❌ NO✅ YES
Browser APIs (window)❌ NO✅ YES
Bundle JS Size0 KB 🚀Depends on the code