How to Share Components Between Server and Client in NextJS


Next.js App Router splits your app into Server Components and Client Components. Server Components run on the server and keep secrets safe. Client Components run in the browser and handle interactivity. The challenge is sharing data and UI between them without breaking the rules of each environment.

This guide shows you how to share components and data between Server and Client Components in Next.js. You’ll learn composition patterns, prop passing rules, and when to use each approach.

Table of Contents

What are Server and Client Components?

In the Next.js App Router, every component is a Server Component by default. Server Components run only on the server. They can fetch data from databases, use API keys, and keep sensitive logic out of the browser. They don’t send JavaScript to the client, which reduces bundle size.

Client Components run on both the server (for the initial HTML) and the client (for interactivity). You mark them with the "use client" directive at the top of the file. They can use useState, useEffect, event handlers, and browser APIs like localStorage and window.

The key rule: Server Components can import and render Client Components, but Client Components can’t import Server Components directly. They can only receive them as props (such as children).

Prerequisites

Before you follow along, you should have:

  • Basic familiarity with React (components, props, hooks)

  • A Next.js project using the App Router (Next.js 13 or later)

  • Node.js installed (version 18 or later recommended)

If you don’t have a Next.js project yet, create one with:

npx create-next-app@latest my-app

How to Pass Data from Server to Client via Props

The simplest way to share data between Server and Client Components is to pass it as props. The Server Component fetches the data, and the Client Component receives it and handles interactivity.

Here is a basic example. A page (Server Component) fetches a post and passes the like count to a LikeButton (Client Component):

// app/post/[id]/page.jsx (Server Component)
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';

export default async function PostPage({ params }) {
  const { id } = await params;
  const post = await getPost(id);

  return (
    
  );
}
// app/ui/like-button.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function LikeButton({ likes, postId }) {
  const [count, setCount] = useState(likes);

  const handleLike = () => {
    setCount((c) => c + 1);
    // Call API or Server Action to persist
  };

  return (
    
  );
}

The Server Component fetches data on the server. The Client Component receives plain values (likes, postId) and manages state and events. This pattern keeps data fetching on the server and interactivity on the client.

How to Pass Server Components as Children to Client Components

You can pass a Server Component as the children prop (or any prop) to a Client Component. The Server Component still renders on the server. The Client Component receives the rendered output, not the component code.

This is useful when you want a Client Component to wrap or control the layout of server-rendered content. For example, a modal that shows server-fetched data:

// app/ui/modal.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function Modal({ children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      
      {isOpen && (
        
      )}
    
  );
}
// app/cart/page.jsx (Server Component)
import Modal from '@/app/ui/modal';
import Cart from '@/app/ui/cart';

export default function CartPage() {
  return (
    
      
    
  );
}
// app/ui/cart.jsx (Server Component - no 'use client')
import { getCart } from '@/lib/cart';

export default async function Cart() {
  const items = await getCart();

  return (
    
    {items.map((item) => (
  • {item.name}
  • ))}
); }

Cart is a Server Component that fetches cart data. It’s passed as children to Modal, which is a Client Component. The server renders Cart first. The RSC Payload includes the rendered result. The client receives that output and displays it inside the modal. The cart data never runs on the client.

You can use the same pattern with named props (slots):

// app/ui/tabs.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function Tabs({ tabs, children }) {
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    

{tabs.map((tab, i) => ( ))}

{children[activeIndex]}

); }
// app/dashboard/page.jsx (Server Component)
import Tabs from '@/app/ui/tabs';
import Overview from '@/app/ui/overview';
import Analytics from '@/app/ui/analytics';

export default function DashboardPage() {
  const tabs = [
    { id: 'overview', label: 'Overview' },
    { id: 'analytics', label: 'Analytics' },
  ];

  return (
    
      
      
    
  );
}

Overview and Analytics can be Server Components that fetch their own data. They render on the server, and the client receives the pre-rendered output.

What Props Are Allowed Between Server and Client

Props passed from Server Components to Client Components must be serializable. React serializes them into the RSC Payload so they can be sent to the client.

Allowed Types

  • Strings, numbers, booleans

  • null and undefined

  • Plain objects (no functions, no class instances)

  • Arrays of serializable values

  • JSX (Server Components as children or other props)

  • Server Actions (functions with "use server")

Not Allowed

  • Functions (except Server Actions)

  • Date objects

  • Class instances

  • Symbols

  • Map, Set, WeakMap, WeakSet

  • Objects with custom prototypes

  • Buffers, ArrayBuffer, typed arrays

If you need to pass a Date, convert it to a string or number first:


If you use MongoDB, convert ObjectId to a string:


Passing Server Actions as props

Server Actions are async functions marked with "use server". You can pass them as props to Client Components. They are serialized by reference, not by value.

// app/actions/post.js
'use server';

export async function likePost(postId) {
  // Update database
  revalidatePath(`/post/${postId}`);
}
// app/post/[id]/page.jsx (Server Component)
import LikeButton from '@/app/ui/like-button';
import { likePost } from '@/app/actions/post';

export default async function PostPage({ params }) {
  const post = await getPost((await params).id);

  return ;
}
// app/ui/like-button.jsx (Client Component)
'use client';

export default function LikeButton({ likes, postId, onLike }) {
  const handleClick = () => {
    onLike(postId);
  };

  return ;
}

You can also bind arguments:


How to Share Data with Context and React.cache

React Context doesn’t work in Server Components. To share data between Server and Client Components, you can combine a Client Component context provider with React.cache for server-side memoization.

Create a cached fetch function:

// lib/user.js
import { cache } from 'react';

export const getUser = cache(async () => {
  const res = await fetch('https://api.example.com/user');
  return res.json();
});

Create a provider that accepts a promise and stores it in context:

// app/providers/user-provider.jsx
'use client';

import { createContext } from 'react';

export const UserContext = createContext(null);

export default function UserProvider({ children, userPromise }) {
  return (
    
      {children}
    
  );
}

In your root layout, pass the promise without awaiting it:

// app/layout.jsx
import UserProvider from '@/app/providers/user-provider';
import { getUser } from '@/lib/user';

export default function RootLayout({ children }) {
  const userPromise = getUser();

  return (
    
      
        {children}
      
    
  );
}

Client Components use use() to unwrap the promise:

// app/ui/profile.jsx
'use client';

import { use, useContext } from 'react';
import { UserContext } from '@/app/providers/user-provider';

export default function Profile() {
  const userPromise = useContext(UserContext);
  if (!userPromise) {
    throw new Error('Profile must be used within UserProvider');
  }
  const user = use(userPromise);

  return 

Welcome, {user.name}

; }

Wrap the component in Suspense for loading states:

// app/dashboard/page.jsx
import { Suspense } from 'react';
import Profile from '@/app/ui/profile';

export default function DashboardPage() {
  return (
    Loading profile...



Source link

Leave a Comment

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

Scroll to Top