Manoj
←Back to Blog
·4 min read

React Server Components: Deciding Where Your Code Should Run

Choose sensible server and client boundaries in the Next.js App Router without moving an entire page into the browser.

reactnextjsserver-componentsfrontend

React Server Components: Deciding Where Your Code Should Run

A page can display database content, contain a search field, and offer an interactive button. Those requirements do not mean every part of the page needs the same execution environment.

The useful question is where each responsibility belongs. Keep data access close to its source, keep browser interactions close to the elements that need them, and make the boundary between the two easy to see.

Start from the App Router default

In the Next.js App Router, layouts and pages are Server Components by default. They can load data on the server without shipping that data-access implementation to the browser.

Client Components are appropriate for state, event handlers, effects, and browser APIs. Next.js describes these responsibilities in its Server and Client Components guide.

For a project detail page, the description, dates, and technology list can be rendered on the server. A small button that copies the project URL needs browser behavior. Keeping those concerns separate is usually easier than making the entire page interactive from the top.

Put the client boundary around the interaction

A copy button can own its state and browser API call:

"use client";

import { useState } from "react";

export const CopyLink = ({ url }: { url: string }) => {
  const [status, setStatus] = useState("Copy link");

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(url);
      setStatus("Copied");
    } catch {
      setStatus("Unable to copy");
    }
  };

  return (
    <button type="button" onClick={copy}>
      <span aria-live="polite">{status}</span>
    </button>
  );
};

The surrounding server-rendered page passes a public URL as a prop. It does not need access to the clipboard, and the button does not need the database client.

The example handles failure because browser capabilities can be unavailable or restricted. The same boundary principle applies to a filter dropdown, a dialog, or a theme toggle: give the interactive component the minimum information it needs.

The directive affects imports

Adding "use client" establishes a client module boundary. Modules imported through that boundary can become part of the client graph, so the decision affects more than the component's own file.

Do not import a server-only database helper into an interactive component and assume it stays on the server because the file name sounds like a backend utility. Keep the data-loading module outside that import path and pass an appropriate result instead.

Composition can preserve the separation. A Server Component can render a Client Component wrapper and pass server-rendered content as children. That differs from the client module importing and executing the server implementation itself.

Client does not mean browser-only rendering

A Client Component can participate in server-generated HTML on an initial page load and then become interactive through hydration. Reading window during rendering can therefore fail even in a file marked "use client".

Use browser APIs in event handlers or the appropriate lifecycle logic. Keep initial output consistent between server rendering and hydration rather than using environment checks to produce unrelated markup on each side.

This distinction also helps performance discussions. Server Components and server rendering are related ideas, but they are not interchangeable labels for the same mechanism.

Pass a view model across the boundary

Prefer a small, explicit shape for client props. For example, a save button may need a resource ID and a saved flag; it does not need the full database row or internal account configuration.

Values crossing the boundary must be serializable by React's supported mechanism. Avoid passing arbitrary closures, database connections, or class instances as ordinary props. Use the framework's supported server-function mechanisms when an interaction must perform server work.

Authorization remains a server responsibility. Hiding a button does not protect the action behind it, and moving a component to the server does not automatically establish a permission check.

Measure the result instead of counting directives

A smaller client boundary can reduce the code a browser needs, but file organization alone does not guarantee a faster page. Check the actual bundle, network activity, and interaction behavior.

Test initial navigation, client navigation, loading states, and failure states. Confirm that users can still understand a failed interaction and that sensitive data is absent from client props.

The best boundary follows the work: render data where it is available, run interactions where the user performs them, and connect the two with a deliberate interface.