Skip to main content

Learning Objectives

By the end of this section, you will be able to:
  • Distinguish between server and client components in Next.js
  • Identify which VSL Service Center components should be server vs client components
  • Optimize component architecture for performance and user experience
  • Implement proper patterns for data fetching and interactivity
  • Apply best practices for component composition and reusability
Duration: 3-4 hours

Part 1: Strategic Analysis & Understanding

Understanding Server vs Client Components

Overview

Next.js App Router introduces a new component model that distinguishes between server and client components. This distinction is crucial for optimizing performance, reducing bundle size, and improving user experience.

Server Components (Default)

Server components run on the server and are rendered before being sent to the browser. They have access to server-side resources and don’t include JavaScript in the client bundle.

Key Characteristics

Server-Side Rendering

  • Rendered on the server
  • HTML sent to browser
  • No JavaScript in client bundle
  • Direct database access

Performance Benefits

  • Reduced bundle size
  • Faster initial page load
  • Better SEO
  • Enhanced security

When to Use Server Components

  • Data Fetching: Fetch data from databases, APIs, or file systems
  • Static Content: Display content that doesn’t change frequently
  • Large Dependencies: Use libraries that are large or server-only
  • Sensitive Operations: Handle operations that shouldn’t be exposed to the client

Example: Server Component

// app/dashboard/page.tsx (Server Component)
import { db } from "@/lib/database";

export default async function Dashboard() {
  // Direct database access on the server
  const stats = await db.getDashboardStats();

  return (
    <div>
      <h1>Dashboard</h1>
      <div className="stats-grid">
        {stats.map((stat) => (
          <div key={stat.id} className="stat-card">
            <h3>{stat.title}</h3>
            <p>{stat.value}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Client Components

Client components run in the browser and provide interactivity. They include JavaScript in the client bundle and can use browser APIs and React hooks.

Key Characteristics

Client-Side Rendering

  • Rendered in the browser
  • Includes JavaScript bundle
  • Hydration required
  • Interactive features

Use Cases

  • Event handlers
  • State management
  • Browser APIs
  • Real-time updates

When to Use Client Components

  • Interactivity: Components that need event handlers (onClick, onChange)
  • Browser APIs: Access to localStorage, geolocation, camera, etc.
  • State Management: Components using useState, useEffect, or other hooks
  • Third-Party Libraries: Libraries that require browser APIs

Example: Client Component

"use client";

import { useState, useEffect } from "react";

export default function InteractiveCounter() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Access browser APIs
    const savedUser = localStorage.getItem("user");
    if (savedUser) {
      setUser(JSON.parse(savedUser));
    }
  }, []);

  const handleIncrement = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h2>Interactive Counter</h2>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      {user && <p>Welcome, {user.name}!</p>}
    </div>
  );
}

The ‘use client’ Directive

The 'use client' directive tells Next.js that a component should run on the client side.

Important Rules

  • Must be at the top: The directive must be the very first line in the file
  • No code before it: Nothing can come before the directive
  • File-level scope: Applies to the entire file and all its exports
"use client"; // Must be first line

import { useState } from "react";

export default function ClientComponent() {
  const [state, setState] = useState("");
  return <div>Interactive content</div>;
}

Basic Component Composition

Server Component with Client Children

// Server Component (app/dashboard/page.tsx)
import { db } from "@/lib/database";
import InteractiveChart from "@/components/InteractiveChart"; // Client component

export default async function Dashboard() {
  const data = await db.getChartData();

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Server-rendered content */}
      <div className="stats">
        <p>Total Users: {data.totalUsers}</p>
        <p>Active Sessions: {data.activeSessions}</p>
      </div>

      {/* Client component for interactivity */}
      <InteractiveChart data={data} />
    </div>
  );
}

Best Practices for Module 1

Component Design

  1. Start with Server Components: Default to server components unless client features are needed
  2. Minimize Client Boundaries: Keep client components small and focused
  3. Use Composition: Compose server and client components effectively
  4. Add ‘use client’ Only When Necessary: Only use the directive when you need interactivity
Key Takeaway: The key to optimal Next.js performance is using server components by default and only adding client components where interactivity is required. This approach reduces bundle size and improves user experience.

Server Components (Default)

Server components run on the server and are rendered before being sent to the browser. They have access to server-side resources and don’t include JavaScript in the client bundle.

Server Component Benefits

  • Reduced JavaScript bundle size - Direct access to databases and APIs - Better SEO and performance - Enhanced security - No hydration required

Server Component Limitations

  • No browser APIs (localStorage, window, etc.) - No event handlers (onClick, onChange) - No React hooks (useState, useEffect) - No third-party libraries that require browser APIs

Client Components

Client components run in the browser and provide interactivity. They include JavaScript in the client bundle and can use browser APIs and React hooks.

Client Component Benefits

  • Full interactivity and event handling - Access to browser APIs - State management with hooks - Real-time updates - Third-party library support

Client Component Limitations

  • Larger JavaScript bundle - Hydration required - No direct server resource access - Potential performance impact

VSL Service Center Component Analysis

Let’s analyze the VSL Service Center components and determine which should be server vs client components:

Server Component Candidates

Components: Card.jsx, Badge.jsx, Stat.jsx Reason: Pure presentation components with no interactivity Migration: Direct conversion to server components
// Server Component - No 'use client' needed
import React from 'react';

interface CardProps {
title?: string;
children: React.ReactNode;
}

export default function Card({ title, children }: CardProps) {
  return (
    <div className="bg-white rounded-lg shadow-md p-4">
      {title && <h3 className="font-medium mb-2">{title}</h3>}
      {children}
    </div>
  );
}
Components: Dashboard.js, Report components Reason: Display data fetched from server, minimal interactivity Migration: Server components with data fetching
// Server Component with data fetching
import { db } from '@/lib/database';

export default async function Dashboard() {
  const stats = await db.getDashboardStats();
  
  return (
    <div className="grid grid-cols-4 gap-4">
      {stats.map(stat => (
        <div key={stat.id} className="bg-white p-4 rounded-lg">
          <h3 className="text-lg font-semibold">{stat.title}</h3>
          <p className="text-2xl font-bold">{stat.value}</p>
        </div>
      ))}
    </div>
  );
}
Components: Footer.js, static parts of Navbar Reason: Static content that doesn’t need interactivity Migration: Server components for static parts
// Server Component for static footer
export default function Footer() {
  return (
    <footer className="bg-gray-800 text-white py-8">
      <div className="container mx-auto px-4">
        <div className="grid grid-cols-3 gap-8">
          <div>
            <h3 className="text-lg font-semibold mb-4">Company</h3>
            <p>VSL Service Center</p>
            <p>Warehouse Management System</p>
          </div>
          <div>
            <h3 className="text-lg font-semibold mb-4">Contact</h3>
            <p>Email: info@vsl.com</p>
            <p>Phone: +1-234-567-8900</p>
          </div>
          <div>
            <h3 className="text-lg font-semibold mb-4">Links</h3>
            <p>Privacy Policy</p>
            <p>Terms of Service</p>
          </div>
        </div>
      </div>
    </footer>
  );
}

Client Component Candidates

Components: Navbar.js (interactive parts) Reason: Dropdown menus, user interactions, localStorage access Migration: Client component with ‘use client’
'use client';

import { useState, useEffect } from 'react';
import Link from 'next/link';

export default function Navbar() {
  const [dropdowns, setDropdowns] = useState<Record<number, boolean>>({});
  const [username, setUsername] = useState('');

useEffect(() => {
const user_id = localStorage.getItem('user_id');
setUsername(user_id || '');
}, []);

const toggleDropdown = (menuId: number) => {
setDropdowns(prev => ({
...prev,
[menuId]: !prev[menuId]
}));
};

return (

<nav className="bg-white shadow-lg">{/* Navigation implementation */}</nav>
); }

Components: UserLogin.js, CreateUser.js, MaterialMaster.js Reason: Form handling, validation, user input Migration: Client components with form handling
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function LoginForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [isLoading, setIsLoading] = useState(false);
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      if (response.ok) {
        router.push('/dashboard');
      }
    } catch (error) {
      console.error('Login failed:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={formData.password}
        onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
        placeholder="Password"
        required
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}
Components: BarcodeScanner.js, WebcamCapture.js Reason: Browser APIs, real-time interactions, device access Migration: Client components with browser API access
'use client';

import { useState, useRef, useEffect } from 'react';

export default function BarcodeScanner() {
  const [isScanning, setIsScanning] = useState(false);
  const [scannedData, setScannedData] = useState('');
  const videoRef = useRef<HTMLVideoElement>(null);

const startScanning = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
if (videoRef.current) {
videoRef.current.srcObject = stream;
setIsScanning(true);
}
} catch (error) {
console.error('Error accessing camera:', error);
}
};

const stopScanning = () => {
if (videoRef.current?.srcObject) {
const stream = videoRef.current.srcObject as MediaStream;
stream.getTracks().forEach(track => track.stop());
setIsScanning(false);
}
};

return (

<div className="barcode-scanner">
  <video ref={videoRef} autoPlay className="w-full h-64 bg-gray-200" />
  <div className="mt-4 space-x-2">
    <button onClick={startScanning} disabled={isScanning}>
      Start Scanning
    </button>
    <button onClick={stopScanning} disabled={!isScanning}>
      Stop Scanning
    </button>
  </div>
  {scannedData && (
    <div className="mt-4 p-4 bg-green-100 rounded">
      <p>Scanned: {scannedData}</p>
    </div>
  )}
</div>
); }

Hybrid Component Patterns

Server Component with Client Component Children

You can compose server and client components together for optimal performance:
// Server Component (Dashboard)
import { db } from '@/lib/database';
import InteractiveChart from '@/components/InteractiveChart'; // Client component
import StatCard from '@/components/StatCard'; // Server component

export default async function Dashboard() {
  const stats = await db.getDashboardStats();
  const chartData = await db.getChartData();

  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Server component for static data */}
      <div className="stats-grid">
        {stats.map(stat => (
          <StatCard key={stat.id} stat={stat} />
        ))}
      </div>

      {/* Client component for interactivity */}
      <InteractiveChart data={chartData} />
    </div>
  );
}

Client Component with Server Component Children

"use client";

import { useState } from "react";
import UserProfile from "@/components/UserProfile"; // Server component

export default function UserDashboard() {
  const [selectedUserId, setSelectedUserId] = useState<string>("");

  return (
    <div className="user-dashboard">
      <div className="user-list">
        {/* Interactive user selection */}
        <button onClick={() => setSelectedUserId("user1")}>
          Select User 1
        </button>
        <button onClick={() => setSelectedUserId("user2")}>
          Select User 2
        </button>
      </div>

      {/* Server component for user data */}
      {selectedUserId && <UserProfile userId={selectedUserId} />}
    </div>
  );
}

Part 2: Hands-On Implementation

Component Optimization Exercises

Exercise 1: Analyze and Categorize Components

  1. Review the VSL Service Center components and categorize them:
    // Create: src/lib/component-analysis.ts
    interface ComponentAnalysis {
      name: string;
      type: "server" | "client" | "hybrid";
      reason: string;
      dependencies: string[];
      migrationComplexity: "low" | "medium" | "high";
    }
    
    const componentAnalysis: ComponentAnalysis[] = [
      {
        name: "Card",
        type: "server",
        reason: "Pure presentation component with no interactivity",
        dependencies: ["react"],
        migrationComplexity: "low",
      },
      {
        name: "Navbar",
        type: "client",
        reason: "Interactive dropdowns, localStorage access, user state",
        dependencies: ["react", "next/navigation", "localStorage"],
        migrationComplexity: "medium",
      },
      {
        name: "Dashboard",
        type: "server",
        reason: "Data display component, can fetch data on server",
        dependencies: ["react", "database"],
        migrationComplexity: "medium",
      },
      {
        name: "BarcodeScanner",
        type: "client",
        reason: "Browser APIs, camera access, real-time interactions",
        dependencies: ["react", "navigator.mediaDevices"],
        migrationComplexity: "high",
      },
    ];
    

Exercise 2: Optimize the Navbar Component

  1. Split the Navbar into server and client parts:
    // Server Component: app/components/layout/NavbarServer.tsx
    import Link from "next/link";
    import Image from "next/image";
    import logo from "@/public/images/TSSCL_Logo.png";
    import NavbarClient from "./NavbarClient";
    
    export default function NavbarServer() {
      return (
        <nav className="bg-white shadow-lg">
          <div className="max-w-7xl mx-auto px-4">
            <div className="flex justify-between items-center h-16">
              <div className="flex items-center">
                <Link href="/" className="flex items-center">
                  <Image
                    src={logo}
                    alt="TSSCL Logo"
                    width={40}
                    height={40}
                    className="mr-2"
                  />
                  <span className="text-xl font-bold text-gray-800">
                    VSL Service Center
                  </span>
                </Link>
              </div>
    
              {/* Client component for interactive parts */}
              <NavbarClient />
            </div>
          </div>
        </nav>
      );
    }
    
    // Client Component: app/components/layout/NavbarClient.tsx
    "use client";
    
    import { useState, useEffect } from "react";
    import { useRouter } from "next/navigation";
    import Link from "next/link";
    
    export default function NavbarClient() {
      const [dropdowns, setDropdowns] = useState<Record<number, boolean>>({});
      const [username, setUsername] = useState("");
      const [menuData, setMenuData] = useState([]);
      const router = useRouter();
    
      useEffect(() => {
        const user_id = localStorage.getItem("user_id");
        setUsername(user_id || "");
        if (user_id) {
          fetchMenuData(user_id);
        }
      }, []);
    
      const fetchMenuData = async (userId: string) => {
        // API call implementation
      };
    
      const handleLogout = () => {
        localStorage.removeItem("user_id");
        localStorage.removeItem("token");
        router.push("/login");
      };
    
      return (
        <>
          <div className="hidden md:flex items-center space-x-4">
            {/* Menu items */}
          </div>
    
          <div className="flex items-center space-x-4">
            <span className="text-gray-700">{username}</span>
            <button
              onClick={handleLogout}
              className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
              Logout
            </button>
          </div>
        </>
      );
    }
    

Exercise 3: Create a Hybrid Dashboard

  1. Create a dashboard that combines server and client components:
    // Server Component: app/dashboard/page.tsx
    import { db } from "@/lib/database";
    import StatCard from "@/components/StatCard";
    import InteractiveChart from "@/components/InteractiveChart";
    
    export default async function DashboardPage() {
      // Fetch data on the server
      const stats = await db.getDashboardStats();
      const chartData = await db.getChartData();
    
      return (
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">Dashboard</h1>
    
          {/* Server-rendered stats */}
          <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
            {stats.map((stat) => (
              <StatCard key={stat.id} stat={stat} />
            ))}
          </div>
    
          {/* Client component for interactivity */}
          <InteractiveChart data={chartData} />
        </div>
      );
    }
    

Part 3: Reflection & Assessment

Core Optimization Concepts

Server vs Client Component Benefits

Server Components

  • Reduced JavaScript bundle size - Better initial page load - Direct database access - Enhanced security

Client Components

  • Full interactivity - Browser API access - State management - Real-time updates

Assessment Criteria

Your component optimization will be evaluated based on:

Architecture Decisions

  • Correct identification of server vs client components - Proper component composition patterns - Optimal performance characteristics - Clean separation of concerns

Implementation Quality

  • Proper TypeScript implementation - Correct use of ‘use client’ directive - Efficient data fetching patterns - Good error handling and loading states

Common Pitfalls & Solutions

Problem: Adding ‘use client’ to components that don’t need it Solution: Start with server components and only add ‘use client’ when necessary
Problem: Trying to use server-side APIs in client components Solution: Use API routes or server actions for server-side operations
Problem: Server and client rendering different content Solution: Ensure consistent rendering between server and client
Problem: Large JavaScript bundles due to unnecessary client components Solution: Audit bundle size and optimize component architecture

Reflection Questions

  1. How did understanding server vs client components change your approach to component design?
    • Consider performance implications
    • Think about bundle size optimization
    • Reflect on user experience improvements
  2. What patterns did you discover for composing server and client components?
    • Hybrid component architectures
    • Data flow optimization
    • State management strategies
  3. What challenges did you face when implementing loading states?
    • User experience considerations
    • Performance optimization
    • Error handling patterns

Extension Activities

  1. Component Architecture Patterns:
    • Research compound component patterns
    • Study render prop and higher-order component patterns
    • Explore custom hooks for shared logic
  2. Advanced Optimization:
    • Study streaming and suspense patterns
    • Explore concurrent features
    • Learn about edge runtime capabilities

Exercise 4: Implement Loading States

  1. Create loading components for different sections:
    // Create: app/dashboard/loading.tsx
    export default function DashboardLoading() {
      return (
        <div className="container mx-auto px-4 py-8">
          <div className="mb-8">
            <div className="h-8 bg-gray-200 rounded w-1/3 animate-pulse"></div>
            <div className="h-4 bg-gray-200 rounded w-1/2 mt-2 animate-pulse"></div>
          </div>
    
          <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
            {Array.from({ length: 4 }).map((_, i) => (
              <div key={i} className="bg-white p-6 rounded-lg shadow-md">
                <div className="h-4 bg-gray-200 rounded w-24 mb-2 animate-pulse"></div>
                <div className="h-8 bg-gray-200 rounded w-16 animate-pulse"></div>
              </div>
            ))}
          </div>
    
          <div className="bg-white rounded-lg shadow-md p-8">
            <div className="h-6 bg-gray-200 rounded w-1/4 mb-4 animate-pulse"></div>
            <div className="space-y-4">
              {Array.from({ length: 5 }).map((_, i) => (
                <div
                  key={i}
                  className="h-4 bg-gray-200 rounded animate-pulse"></div>
              ))}
            </div>
          </div>
        </div>
      );
    }
    
  2. Create master data loading component:
    // Create: app/master/loading.tsx
    export default function MasterLoading() {
      return (
        <div className="container mx-auto px-4 py-8">
          <div className="mb-6">
            <div className="h-8 bg-gray-200 rounded w-1/3 animate-pulse"></div>
            <div className="h-4 bg-gray-200 rounded w-1/2 mt-2 animate-pulse"></div>
          </div>
    
          <div className="mb-8">
            <div className="flex space-x-4">
              {Array.from({ length: 3 }).map((_, i) => (
                <div
                  key={i}
                  className="h-10 bg-gray-200 rounded w-24 animate-pulse"></div>
              ))}
            </div>
          </div>
    
          <div className="bg-white rounded-lg shadow-md p-6">
            <div className="h-6 bg-gray-200 rounded w-1/4 mb-4 animate-pulse"></div>
            <div className="overflow-x-auto">
              <div className="min-w-full">
                <div className="h-12 bg-gray-200 rounded mb-4 animate-pulse"></div>
                {Array.from({ length: 5 }).map((_, i) => (
                  <div
                    key={i}
                    className="h-16 bg-gray-200 rounded mb-2 animate-pulse"></div>
                ))}
              </div>
            </div>
          </div>
        </div>
      );
    }
    
  3. Test loading states by adding delays to your pages:
    // Update: app/dashboard/page.tsx
    export default async function DashboardPage() {
      // Simulate data fetching delay
      await new Promise((resolve) => setTimeout(resolve, 2000));
    
      const stats = [
        {
          title: "Total Customers",
          value: "1,234",
          change: { value: "12%", type: "increase" },
        },
        // ... rest of your data
      ];
    
      return (
        <div className="container mx-auto px-4 py-8">
          <h1 className="text-3xl font-bold mb-8">Dashboard</h1>
          {/* ... rest of your component */}
        </div>
      );
    }
    

Next Steps

After completing this lesson, you’ll be ready to move on to Lesson 1.5: Layouts & Templates, where you’ll:
  • Implement consistent layouts across your application
  • Create reusable template patterns
  • Set up proper layout hierarchies
  • Optimize layout performance
Key Takeaway: The key to optimal Next.js performance is using server components by default and only adding client components where interactivity is required. This approach reduces bundle size and improves user experience.