Node.js

Practical Guide to Sharing Server and Client Components in Next.js

Next.js App Router introduced a powerful concept: Server Components and Client Components. This separation allows developers to optimize performance by defaulting to server-side rendering while selectively enabling interactivity on the client. However, sharing data and components between server and client layers requires understanding clear boundaries. Let us delve into understanding how to share components between server and client in NextJS.

1. What are Server and Client Components?

Server Components are rendered only on the server and are never sent as JavaScript to the browser. They cannot use React hooks like useState or useEffect. Since they execute on the server, they can directly access backend resources such as databases, file systems, and internal APIs without exposing sensitive logic to the client. This helps improve performance, reduce bundle size, and enhance security by keeping server-side logic off the client. Key benefits of Server Components include:

  • Improved performance (less JavaScript sent to client)
  • Better security (no exposure of server logic)
  • Direct data access without API layers
  • Reduced client-side hydration cost

Client Components run in the browser and are responsible for interactivity. They support state, effects, event handlers, and all browser-based APIs. Any component that needs user interaction must be marked explicitly with the "use client" directive, which tells Next.js to include it in the client-side bundle. They support:

  • State management (useState)
  • Side effects (useEffect)
  • Event handlers (onClick, onChange, etc.)
  • Browser APIs (window, localStorage, etc.)

To mark a component as client-side, you must explicitly add: "use client".

1.1 What Props Are Allowed Between Server and Client?

Server to Client props must be serializable because data is transferred over the network boundary. You can safely pass primitive values like strings, numbers, and booleans, as well as arrays and plain JavaScript objects. You can also pass structured data like JSON that has been serialized on the server.

//Example of valid props

{
  id: 1,
  name: "John Doe",
  isActive: true,
  roles: ["admin", "editor"]
}

However, you cannot pass non-serializable values such as functions, class instances, database connections, or complex runtime objects. Even objects like Date are not directly supported unless they are converted into a serializable format (for example, ISO strings or timestamps) before being passed to the client.

2. Code Example

This single example demonstrates all key patterns in Next.js App Router, including passing data from Server to Client via props, passing Server Components as children to Client Components, sharing data using React.cache, safely using third-party client-only libraries, and preventing environment leakage using server-only and client-only utilities.

2.1 Server-Only Data Layer and Cached Server Fetching in Next.js

2.1.1 Server-Only Secure Utility Using server-only Module

While use client defines boundaries, it doesn’t prevent a developer from accidentally importing a “server-side” utility into a Client Component. To prevent sensitive data (like API keys) from ever leaking to the browser, we use the server-only package.

Note: This is a package you install via npm install server-only. It is not a directive, but a “poison pill” that triggers a build error if this file is imported into a Client Component.

// lib/server-only.ts
import "server-only";

export async function getSecretToken() {
  return "SUPER_SECRET_TOKEN";
}

This code defines a server-only utility in Next.js by importing server-only, which ensures the module is restricted to server execution and cannot be bundled into client-side code. It then exports an async function getSecretToken that returns a sensitive value, SUPER_SECRET_TOKEN, representing secure server-side data that should never be exposed to the browser, helping enforce safe separation between server logic and client components.

2.1.2 Cached Server Data Fetching with React.cache for Performance Optimization

This layer handles server-side data fetching with caching to avoid redundant requests and improve performance across renders.

// lib/data.ts
import { cache } from "react";
import { getSecretToken } from "./server-only";

export const getProducts = cache(async () => {
  const token = await getSecretToken();

  const res = await fetch("https://api.example.com/products?token=" + token);
  return res.json();
});

export const getStats = cache(async () => {
  return {
    users: 1200,
    revenue: 45000
  };
});

This code defines reusable server-side data utilities in Next.js using React’s cache function to memoize results and avoid redundant computations during rendering. The getProducts function first securely retrieves a secret token from the server-only module using getSecretToken, then uses that token to fetch product data from an external API and returns the parsed JSON response. The getStats function provides static aggregated data such as user count and revenue, also wrapped in cache to ensure consistent and efficient reuse across requests, improving performance while keeping sensitive logic confined to the server.

2.2 Server Component as the Main Page Entry Point in Next.js App Router

This section demonstrates how the main page in the App Router acts as a Server Component that fetches data on the server and orchestrates both Server and Client Components in a single flow.

// app/page.tsx (Server Component)

import ClientDashboard from "./components/ClientDashboard";
import ServerStats from "./components/ServerStats";
import { getProducts, getStats } from "./lib/data";

function ServerBanner() {
  return <h2>Server Rendered Banner</h2>;
}

export default async function Page() {
  const products = await getProducts();
  const stats = await getStats();

  return (
    <div>
      <ServerBanner />

      {/* Passing data from server to client */}
      <ClientDashboard stats={stats} products={products}>
        
        {/* Passing Server Component as child */}
        <ServerStats />

      </ClientDashboard>
    </div>
  );
}

This code defines a Next.js App Router page as a Server Component where data is fetched on the server before rendering. Inside the Page function, it asynchronously retrieves product data using getProducts and aggregated statistics using getStats, ensuring both are resolved on the server before the UI is sent to the client. A local ServerBanner component is rendered directly on the server as static content. The fetched data is then passed from the server to the ClientDashboard Client Component via props, demonstrating server-to-client data flow. Additionally, a ServerStats Server Component is passed as a child to the Client Component, showing how Server Components can be composed within Client Components. Overall, this structure combines server-side rendering, data fetching, and hybrid component composition in a single Next.js page.

2.3 Composing Server Components Inside Client Components Using the Children Pattern

This section demonstrates how Server Components can be composed inside Client Components by passing them as children, allowing server-rendered UI to be seamlessly embedded within client-side interactive layouts.

// app/components/ServerStats.tsx (Server Component)

import { getStats } from "../lib/data";

export default async function ServerStats() {
  const stats = await getStats();

  return (
    <div>
      <h3>Server Stats (Server Component)</h3>
      <p>Users: {stats.users}</p>
      <p>Revenue: {stats.revenue}</p>
    </div>
  );
}

This code defines a Server Component named ServerStats in Next.js that runs entirely on the server and fetches data before rendering. It imports the getStats function from the data layer and calls it asynchronously to retrieve server-side statistics such as users and revenue. Once the data is resolved, it renders a simple UI containing a heading and two values displayed using JSX expressions. Since this is a Server Component, it does not include any client-side interactivity or React hooks and is executed only on the server, ensuring efficient data fetching and secure handling of backend logic before the HTML is sent to the browser.

2.4 Passing Server Components as Children into Client Components for Composition

This section shows how a Client Component can act as a wrapper that receives server-fetched data, renders client-only interactive features, and also embeds Server Components through the children prop for flexible composition.

// app/components/ClientDashboard.tsx

"use client";

import dynamic from "next/dynamic";

// client-only import (third-party chart lib)
const Chart = dynamic(() => import("./Chart"), { ssr: false });

export default function ClientDashboard({ stats, products, children }) {
  return (
    <div style={{ border: "2px solid #ddd", padding: "12px" }}>

      <h2>Client Dashboard</h2>

      {/* Props from server */}
      <p>Total Users: {stats.users}</p>

      {/* Third-party / client-only component */}
      <Chart data={stats} />

      {/* Server Component as children */}
      <div style={{ marginTop: "10px" }}>
        {children}
      </div>

      {/* Server-fetched data rendered on client */}
      <h3>Products</h3>
      <ul>
        {products.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>

    </div>
  );
}

This code defines a Client Component named ClientDashboard in Next.js, marked with the "use client" directive so it runs in the browser and supports interactivity. It receives stats, products, and children as props from a Server Component. Inside the component, it dynamically imports a client-only chart library using next/dynamic with server-side rendering disabled, ensuring the chart is loaded only in the browser. The component then renders server-fetched data such as total users and a list of products, demonstrating how server data is safely passed into a client environment. It also renders children, allowing a Server Component to be composed inside it. Overall, this component showcases how client-side interactivity, dynamic imports, and server-provided data work together in a hybrid Next.js architecture.

2.5 Client-Only Third-Party Library Integration Using Dynamic Imports

Similarly, some libraries or utilities rely strictly on browser APIs (like window or document). If these are accidentally imported into a Server Component, the build will fail. Importing client-only explicitly marks this file as “browser-use only.”

// app/components/Chart.tsx

"use client";

import "client-only";

export default function Chart({ data }) {
  return (
    <div>
      <h4>Revenue Chart (Client Only)</h4>
      <p>Revenue: {data.revenue}</p>
    </div>
  );
}

This code defines a Client Component named Chart in Next.js, explicitly marked with "use client" so it runs only in the browser and supports interactive or UI-heavy functionality. It also imports client-only to ensure the file cannot be accidentally used on the server. The component receives data as a prop, which is expected to come from a Server Component, and it renders a simple UI displaying a revenue value inside a “Revenue Chart” section. In a real-world scenario, this component would typically be replaced with a full-featured charting library, but here it demonstrates the pattern of isolating client-only UI logic while still consuming server-provided data safely.

2.6 Code Run and Output

First, the app/page.tsx Server Component runs on the server and initiates data fetching by calling getProducts and getStats. These functions execute on the server only, where getProducts securely retrieves a token using the server-only utility and fetches product data from an external API, while getStats returns cached static metrics. Once all data is resolved, the server renders the component tree. Next, the server renders ServerBanner and ServerStats as part of the HTML output, while also preparing props for the Client Component ClientDashboard. The serialized stats and products data are passed across the server-to-client boundary.

On the client side, React hydrates ClientDashboard, enabling interactivity. The dashboard receives server-provided props, renders dynamic UI such as product lists, and loads the Chart component using a client-only dynamic import with server-side rendering disabled. This ensures that browser-only libraries are executed safely in the client environment.

Finally, the ServerStats component is rendered on the server but injected into the Client Component as children, demonstrating cross-boundary composition. The result is a hybrid UI where server-rendered content, client interactivity, and optimized data fetching work together seamlessly.

Fig. 1: Code output
Fig. 1: Code output

The final UI displayed in the browser consists of a server-rendered banner, a client-side dashboard showing total users and a list of products, a dynamically loaded revenue chart, and embedded server-rendered statistics. All data is fetched securely on the server, passed safely to the client where needed, and rendered in a single cohesive interface.

3. Conclusion

This complete architecture demonstrates how Next.js cleanly separates server and client concerns, where Server Components are responsible for fetching data and running secure server-side logic, while Client Components handle interactivity and UI state in the browser. The children pattern enables seamless composition across server-client boundaries, React.cache optimizes repeated server calls by reusing results, and the server-only and client-only utilities prevent environment leakage by ensuring code runs only in its intended context. The key principle is simple: Server does the work, Client handles the experience.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button