Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Passing Props Server Client
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Passing Props Between Server and Client

Understanding serialization and data flow across component boundaries

Server and Client Components live in different worlds—one on the server, one in the browser. When you pass props between them, data must cross this boundary through serialization. Not all JavaScript values can make this journey. Some data types work perfectly, others need transformation, and some simply can't cross at all. Understanding these rules is essential for building Next.js applications that work reliably and efficiently. Let's master the art of passing data between Server and Client Components!

The Serialization Boundary

When props cross from Server to Client Components, they're serialized to JSON:

The Journey of Props

  1. Server Component: Creates data
  2. Serialization: Converts to JSON string
  3. Transfer: Sends to browser in HTML
  4. Deserialization: Converts back to JavaScript
  5. Client Component: Receives data

JSON Serialization

Props are serialized using JSON.stringify() and deserialized with JSON.parse(). This means props must be JSON-compatible.

✅ Can Be Serialized

  • Strings
  • Numbers
  • Booleans
  • null
  • Arrays
  • Plain objects

❌ Cannot Be Serialized

  • Functions
  • Date objects (becomes string)
  • undefined
  • Symbol
  • Map / Set
  • Class instances

Basic Prop Passing

Simple Data Types

app/page.tsx
// Server Component
import { ClientComponent } from '@/components/ClientComponent';

export default async function Page() {
  return (
    <ClientComponent
      title="Hello World"           // ✅ String
      count={42}                     // ✅ Number
      isActive={true}                // ✅ Boolean
      tags={['react', 'nextjs']}     // ✅ Array
      config={{ theme: 'dark' }}     // ✅ Plain object
      empty={null}                   // ✅ null
    />
  );
}
components/ClientComponent.tsx
'use client';

interface ClientComponentProps {
  title: string;
  count: number;
  isActive: boolean;
  tags: string[];
  config: { theme: string };
  empty: null;
}

export function ClientComponent({
  title,
  count,
  isActive,
  tags,
  config,
  empty,
}: ClientComponentProps) {
  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
      <p>Active: {isActive ? 'Yes' : 'No'}</p>
      <ul>
        {tags.map(tag => (
          <li key={tag}>{tag}</li>
        ))}
      </ul>
      <p>Theme: {config.theme}</p>
    </div>
  );
}

// ✅ All props are JSON-serializable
// ✅ TypeScript ensures type safety

Complex Data Structures

Nested Objects and Arrays

app/blog/page.tsx
// Server Component
import { BlogList } from '@/components/BlogList';

interface Post {
  id: string;
  title: string;
  author: {
    name: string;
    avatar: string;
    bio: string;
  };
  tags: string[];
  metadata: {
    views: number;
    likes: number;
    comments: number;
  };
}

async function getPosts(): Promise<Post[]> {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Blog Posts</h1>
      {/* Complex nested data structure */}
      <BlogList posts={posts} />
    </div>
  );
}

// ✅ Complex nested structure
// ✅ All values are JSON-serializable
// ✅ TypeScript provides type safety
components/BlogList.tsx
'use client';

interface Author {
  name: string;
  avatar: string;
  bio: string;
}

interface Post {
  id: string;
  title: string;
  author: Author;
  tags: string[];
  metadata: {
    views: number;
    likes: number;
    comments: number;
  };
}

export function BlogList({ posts }: { posts: Post[] }) {
  return (
    <div className="space-y-6">
      {posts.map(post => (
        <article key={post.id} className="border rounded-lg p-6">
          <h2 className="text-2xl font-bold mb-2">{post.title}</h2>
          
          <div className="flex items-center gap-3 mb-4">
            <img
              src={post.author.avatar}
              alt={post.author.name}
              className="w-10 h-10 rounded-full"
            />
            <div>
              <p className="font-semibold">{post.author.name}</p>
              <p className="text-sm text-gray-600">{post.author.bio}</p>
            </div>
          </div>

          <div className="flex gap-2 mb-4">
            {post.tags.map(tag => (
              <span
                key={tag}
                className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
              >
                {tag}
              </span>
            ))}
          </div>

          <div className="flex gap-6 text-sm text-gray-600">
            <span>👁️ {post.metadata.views} views</span>
            <span>❤️ {post.metadata.likes} likes</span>
            <span>💬 {post.metadata.comments} comments</span>
          </div>
        </article>
      ))}
    </div>
  );
}

What Cannot Be Passed

❌ Functions

TYPESCRIPT
// ❌ BAD: Cannot pass functions
function ServerComponent() {
  const handleClick = () => {
    console.log('clicked');
  };

  return <ClientComponent onClick={handleClick} />; // ❌ Error!
}

// ✅ GOOD: Define functions in Client Component
'use client';
function ClientComponent() {
  const handleClick = () => {
    console.log('clicked');
  };

  return <button onClick={handleClick}>Click</button>; // ✅ Works!
}

❌ Class Instances

TYPESCRIPT
class User {
  constructor(public name: string) {}
  greet() {
    return `Hello, ${this.name}`;
  }
}

// ❌ BAD: Cannot pass class instances
function ServerComponent() {
  const user = new User('Alice');
  return <ClientComponent user={user} />; // ❌ Methods lost!
}

// ✅ GOOD: Pass plain object
function ServerComponent() {
  const user = {
    name: 'Alice',
    // Include any needed data as plain values
  };
  return <ClientComponent user={user} />; // ✅ Works!
}

⚠️ Date Objects (Special Case)

Date objects are serialized to ISO strings. You need to convert back to Date in the Client Component:

TYPESCRIPT
// Server Component
function ServerComponent() {
  const post = {
    title: 'My Post',
    publishedAt: new Date('2024-01-15'), // Date object
  };

  return <ClientComponent post={post} />;
}

// What Client Component receives
'use client';
function ClientComponent({ post }) {
  // post.publishedAt is now a STRING, not a Date!
  console.log(typeof post.publishedAt); // "string"
  
  // ✅ Convert back to Date
  const date = new Date(post.publishedAt);
  
  return (
    <div>
      <h2>{post.title}</h2>
      <time>{date.toLocaleDateString()}</time>
    </div>
  );
}

❌ undefined

TYPESCRIPT
// ❌ BAD: undefined becomes null
function ServerComponent() {
  return (
    <ClientComponent
      value={undefined}  // Becomes null in Client Component!
    />
  );
}

// ✅ GOOD: Use null explicitly or omit the prop
function ServerComponent() {
  return (
    <ClientComponent
      value={null}  // Explicit null
      // Or don't pass the prop at all
    />
  );
}

Handling Dates Properly

Pattern 1: Serialize to ISO String

app/events/page.tsx
// Server Component
interface Event {
  id: string;
  title: string;
  startDate: string;  // ISO string
  endDate: string;    // ISO string
}

async function getEvents(): Promise<Event[]> {
  const events = await db.events.findMany();
  
  // Convert Dates to ISO strings
  return events.map(event => ({
    ...event,
    startDate: event.startDate.toISOString(),
    endDate: event.endDate.toISOString(),
  }));
}

export default async function EventsPage() {
  const events = await getEvents();
  
  return <EventList events={events} />;
}
components/EventList.tsx
'use client';

interface Event {
  id: string;
  title: string;
  startDate: string;  // Receives as string
  endDate: string;    // Receives as string
}

export function EventList({ events }: { events: Event[] }) {
  return (
    <div className="space-y-4">
      {events.map(event => {
        // Convert strings back to Dates
        const start = new Date(event.startDate);
        const end = new Date(event.endDate);

        return (
          <div key={event.id} className="border rounded p-4">
            <h3 className="font-bold">{event.title}</h3>
            <p className="text-sm text-gray-600">
              {start.toLocaleDateString()} - {end.toLocaleDateString()}
            </p>
            <p className="text-sm text-gray-600">
              Duration: {Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))} days
            </p>
          </div>
        );
      })}
    </div>
  );
}

Pattern 2: Utility Functions

lib/serialization.ts
// Utility to serialize dates
export function serializeDate(date: Date): string {
  return date.toISOString();
}

// Utility to deserialize dates
export function deserializeDate(dateString: string): Date {
  return new Date(dateString);
}

// Serialize object with dates
export function serializeDates<T extends Record<string, any>>(
  obj: T,
  dateKeys: (keyof T)[]
): T {
  const serialized = { ...obj };
  
  for (const key of dateKeys) {
    if (obj[key] instanceof Date) {
      serialized[key] = obj[key].toISOString() as any;
    }
  }
  
  return serialized;
}

// Usage
const post = {
  title: 'My Post',
  publishedAt: new Date(),
  updatedAt: new Date(),
};

const serialized = serializeDates(post, ['publishedAt', 'updatedAt']);

Handling Large Datasets

Problem: Too Much Data

Passing large datasets as props increases HTML size and hydration time.

Solution 1: Pass Only What's Needed

TYPESCRIPT
// ❌ BAD: Passing entire dataset
async function Page() {
  const products = await db.products.findMany(); // 10,000 products
  
  return <ProductList products={products} />; // ❌ Huge HTML!
}

// ✅ GOOD: Pagination
async function Page({ searchParams }) {
  const page = Number(searchParams.page) || 1;
  const limit = 20;
  
  const products = await db.products.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });
  
  return <ProductList products={products} page={page} />; // ✅ Only 20 items
}

Solution 2: Pass IDs, Fetch Client-Side

TYPESCRIPT
// Server Component - pass only IDs
async function Page() {
  const productIds = await db.products.findMany({
    select: { id: true },
  });
  
  const ids = productIds.map(p => p.id);
  
  return <ProductList productIds={ids} />;
}

// Client Component - fetch details when needed
'use client';
export function ProductList({ productIds }) {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);

  const loadProducts = async (ids: string[]) => {
    setLoading(true);
    const res = await fetch('/api/products', {
      method: 'POST',
      body: JSON.stringify({ ids }),
    });
    const data = await res.json();
    setProducts(data);
    setLoading(false);
  };

  return (
    <div>
      <button onClick={() => loadProducts(productIds.slice(0, 10))}>
        Load First 10
      </button>
      {loading && <p>Loading...</p>}
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Solution 3: Streaming with Suspense

TYPESCRIPT
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      {/* Load immediately */}
      <QuickContent />
      
      {/* Load asynchronously */}
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>
    </div>
  );
}

async function ProductList() {
  const products = await getProducts();
  return (
    <div>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

TypeScript Best Practices

Define Shared Types

types/blog.ts
// Shared types for Server and Client
export interface Author {
  id: string;
  name: string;
  avatar: string;
  bio: string;
}

export interface Post {
  id: string;
  title: string;
  content: string;
  author: Author;
  publishedAt: string;  // ISO string, not Date
  tags: string[];
  metadata: {
    views: number;
    likes: number;
    comments: number;
  };
}

// Type for database (with Date objects)
export interface PostDB {
  id: string;
  title: string;
  content: string;
  author: Author;
  publishedAt: Date;  // Date object in DB
  tags: string[];
  metadata: {
    views: number;
    likes: number;
    comments: number;
  };
}

// Conversion utility
export function serializePost(post: PostDB): Post {
  return {
    ...post,
    publishedAt: post.publishedAt.toISOString(),
  };
}

Use Branded Types for Safety

TYPESCRIPT
// Branded type for ISO date strings
type ISODateString = string & { readonly __brand: 'ISODateString' };

function toISODateString(date: Date): ISODateString {
  return date.toISOString() as ISODateString;
}

interface Event {
  id: string;
  title: string;
  date: ISODateString;  // Clear this is a date string
}

// TypeScript ensures you convert dates properly
const event: Event = {
  id: '1',
  title: 'Conference',
  date: toISODateString(new Date()),  // ✅ Must convert
  // date: '2024-01-15'  // ❌ TypeScript error without conversion
};

Validate Props at Runtime

TYPESCRIPT
'use client';

import { z } from 'zod';

// Define schema
const PostSchema = z.object({
  id: z.string(),
  title: z.string(),
  content: z.string(),
  publishedAt: z.string().datetime(),  // Validates ISO format
  tags: z.array(z.string()),
});

type Post = z.infer<typeof PostSchema>;

export function BlogPost({ post }: { post: Post }) {
  // Validate at runtime (optional but helpful)
  const validated = PostSchema.parse(post);
  
  return (
    <article>
      <h1>{validated.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: validated.content }} />
    </article>
  );
}

Common Patterns

Pattern 1: Configuration Objects

TYPESCRIPT
// Server Component
function Page() {
  const config = {
    theme: 'dark',
    language: 'en',
    features: {
      comments: true,
      sharing: true,
      analytics: false,
    },
  };

  return <App config={config} />;
}

// Client Component uses config for behavior
'use client';
export function App({ config }) {
  return (
    <div className={config.theme === 'dark' ? 'dark' : 'light'}>
      {config.features.comments && <Comments />}
      {config.features.sharing && <ShareButtons />}
    </div>
  );
}

Pattern 2: Initial State

TYPESCRIPT
// Server Component fetches initial data
async function Page() {
  const initialTodos = await getTodos();
  
  return <TodoList initialTodos={initialTodos} />;
}

// Client Component uses initial data, then manages state
'use client';
import { useState } from 'react';

export function TodoList({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  
  const addTodo = (text: string) => {
    setTodos([...todos, { id: Date.now(), text, done: false }]);
  };
  
  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>{todo.text}</div>
      ))}
      <button onClick={() => addTodo('New todo')}>Add</button>
    </div>
  );
}

Pattern 3: Metadata for Client Logic

TYPESCRIPT
// Server Component
async function Page() {
  const user = await getUser();
  
  // Pass metadata for client-side logic
  return (
    <Dashboard
      userId={user.id}
      isPremium={user.isPremium}
      permissions={user.permissions}
      preferences={user.preferences}
    />
  );
}

// Client Component uses metadata to determine behavior
'use client';
export function Dashboard({ userId, isPremium, permissions, preferences }) {
  return (
    <div>
      {permissions.includes('admin') && <AdminPanel />}
      {isPremium ? <PremiumFeatures /> : <FreeFeatures />}
      <UserSettings preferences={preferences} />
    </div>
  );
}

Debugging Serialization Issues

Error: Objects Are Not Valid

TYPESCRIPT
// ❌ Common error
// Error: Objects are not valid as a React child

// Problem: Trying to render an object directly
function Component({ data }) {
  return <div>{data}</div>; // ❌ If data is object
}

// ✅ Solution: Access properties
function Component({ data }) {
  return <div>{data.title}</div>; // ✅ Render string
}

// ✅ Or use JSON.stringify for debugging
function Component({ data }) {
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Check Serialization Manually

TYPESCRIPT
// Test if data can be serialized
const data = {
  title: 'Test',
  date: new Date(),
  onClick: () => {}, // ❌ Function!
};

try {
  const serialized = JSON.stringify(data);
  const deserialized = JSON.parse(serialized);
  console.log(deserialized);
  // date is now a string
  // onClick is missing (functions can't be serialized)
} catch (error) {
  console.error('Cannot serialize:', error);
}

Add Development Warnings

TYPESCRIPT
'use client';

export function ClientComponent({ data }) {
  // Warn in development if data isn't what you expect
  if (process.env.NODE_ENV === 'development') {
    if (typeof data.date === 'string') {
      console.warn('date prop is string, convert to Date:', data.date);
    }
    if (data.onClick) {
      console.warn('onClick prop should be defined in Client Component');
    }
  }
  
  return <div>{data.title}</div>;
}

Serialization Examples

Examples of passing different data types between components

examplesImportant

Select a file or folder to see details

Key Takeaways

  • Props are serialized to JSON - must be JSON-compatible
  • Simple types work fine - strings, numbers, booleans, arrays, objects
  • Functions cannot be passed - define in Client Component
  • Dates become strings - convert back with new Date()
  • Class instances lose methods - pass plain objects
  • undefined becomes null - use null explicitly
  • Large datasets need strategies - pagination, streaming, lazy loading
  • Use TypeScript - shared types ensure consistency

What's Next?

You've mastered prop passing and serialization between Server and Client Components! The final lesson in this series covers Server Component Patterns and Best Practices—advanced patterns, performance optimization, and architectural guidance for building production-ready applications.

You'll learn patterns like data fetching strategies, caching patterns, error handling approaches, and how to structure large applications for maintainability and performance. This is where everything comes together!

🔍 Debug Early

When props aren't working as expected, check serialization first. A quick JSON.stringify() test can save hours of debugging. Remember: if it can't be JSON.stringify'd, it can't cross the Server-Client boundary!

Test Your Understanding

Question 1 of 4

What happens to props passed from Server to Client Components?

Master prop passing and serialization in Next.js! Learn what data can cross the Server-Client boundary.

Previous
Component Composition Patterns
Next
Server Component Patterns and Best Practices

Master Next.js Server Components

Join 2,000+ developers building production Next.js applications. Get the final lesson on Server Component patterns - 100% FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo