Next.js: Server and Client Components in a Real Application

Next.js App Router applications use React Server Components to render UI on the server and Client Components for functionality that requires browser-side interactivity. Understanding the boundary between them helps developers improve performance, simplify data access, and reduce unnecessary client-side JavaScript.

Start with Server Components

In the App Router, components are Server Components by default unless a client boundary is introduced.

A Server Component can retrieve data directly from a backend service:

export default async function ProductsPage() {
  const response = await fetch(
    "https://api.example.com/products"
  );

  if (!response.ok) {
    throw new Error("Unable to load products.");
  }

  const products = await response.json();

  return (
    <main>
      <h1>Products</h1>

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

This example demonstrates the component pattern; the API URL, authentication, caching behavior, and error handling should be adapted to the application.

Server Components are useful for data access, server-side rendering, and keeping server-only dependencies out of the client bundle.

Introduce Client Components where needed

Interactive features such as local state, click handlers, and browser APIs require Client Components.

"use client";

import { useState } from "react";

export default function QuantityControl() {
  const [quantity, setQuantity] = useState(1);

  return (
    <div>
      <button
        type="button"
        onClick={() => setQuantity((value) =>
          Math.max(1, value - 1)
        )}
      >
        −
      </button>

      <span>{quantity}</span>

      <button
        type="button"
        onClick={() => setQuantity((value) => value + 1)}
      >
        +
      </button>
    </div>
  );
}

The “use client” directive marks a module boundary. Its imported component dependencies become part of the client-side module graph, so placing this directive too high in the component tree can unnecessarily increase client JavaScript.

Keep the client boundary small

A page can combine server-rendered content with interactive components:

import QuantityControl from "./QuantityControl";

export default async function ProductPage() {
  const product = await getProduct();

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <QuantityControl />
    </main>
  );
}

Here, getProduct represents an application-specific server-side data function. The page can retrieve product data on the server while the quantity control handles interaction in the browser.

Protect sensitive information

Server Components are not automatically safe simply because they execute on the server. Avoid passing secrets, private database records, or unnecessary sensitive data to Client Components.

Keep privileged operations on the server and verify authorization for every protected operation. Hiding a button in the UI is not a substitute for enforcing permissions in the backend.

Understand caching

Server-side fetching and rendering do not mean every request is automatically fresh or automatically cached. Caching and revalidation behavior depends on the Next.js version, rendering strategy, and explicit configuration.

Review caching requirements for each data source, particularly when responses contain user-specific information.

Conclusion

Server Components should generally handle server-side data access and noninteractive UI, while Client Components should handle the interactions that require browser capabilities. Keeping this boundary deliberate improves maintainability, security, and performance.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *