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

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

Client Component Candidates

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

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.