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
Part 1: Strategic Analysis & Understanding
Understanding Server vs Client Components
Review: Server and 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
Start with Server Components : Default to server components unless client features are needed
Minimize Client Boundaries : Keep client components small and focused
Use Composition : Compose server and client components effectively
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 >
);
}
Layout Components (Partial)
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 : 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
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
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
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
Overusing Client Components
Problem : Adding ‘use client’ to components that don’t need it
Solution : Start with server components and only add ‘use client’ when necessary
Mixing Server and Client Logic
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
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
What patterns did you discover for composing server and client components?
Hybrid component architectures
Data flow optimization
State management strategies
What challenges did you face when implementing loading states?
User experience considerations
Performance optimization
Error handling patterns
Extension Activities
Component Architecture Patterns:
Research compound component patterns
Study render prop and higher-order component patterns
Explore custom hooks for shared logic
Advanced Optimization:
Study streaming and suspense patterns
Explore concurrent features
Learn about edge runtime capabilities
Exercise 4: Implement Loading States
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 >
);
}
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 >
);
}
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.