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. 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 run in the browser and provide interactivity. They include JavaScript in the client bundle and can use browser APIs and React hooks. 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 >
);
}
Component Composition Patterns
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 >
);
}
Client Component with Server 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 >
< div className = "user-selector" >
< 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 >
);
}
"use client" ;
import { useState } from "react" ;
import { useRouter } from "next/navigation" ;
export default function ContactForm () {
const [ formData , setFormData ] = useState ({
name: "" ,
email: "" ,
message: "" ,
});
const [ isSubmitting , setIsSubmitting ] = useState ( false );
const router = useRouter ();
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ();
setIsSubmitting ( true );
try {
const response = await fetch ( "/api/contact" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ( formData ),
});
if ( response . ok ) {
router . push ( "/thank-you" );
}
} catch ( error ) {
console . error ( "Error submitting form:" , error );
} finally {
setIsSubmitting ( false );
}
};
return (
< form onSubmit = { handleSubmit } >
< input
type = "text"
value = {formData. name }
onChange = {(e) =>
setFormData (( prev ) => ({ ... prev , name : e . target . value }))
}
placeholder = "Name"
required
/>
< input
type = "email"
value = {formData. email }
onChange = {(e) =>
setFormData (( prev ) => ({ ... prev , email : e . target . value }))
}
placeholder = "Email"
required
/>
< textarea
value = {formData. message }
onChange = {(e) =>
setFormData (( prev ) => ({ ... prev , message : e . target . value }))
}
placeholder = "Message"
required
/>
< button type = "submit" disabled = { isSubmitting } >
{ isSubmitting ? "Sending..." : "Send Message" }
</ button >
</ form >
);
}
Real-Time Updates "use client" ;
import { useState , useEffect } from "react" ;
export default function LiveStats () {
const [ stats , setStats ] = useState ({
onlineUsers: 0 ,
totalRequests: 0 ,
});
useEffect (() => {
// WebSocket connection for real-time updates
const ws = new WebSocket ( "ws://localhost:8080" );
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
setStats ( data );
};
return () => ws . close ();
}, []);
return (
< div className = "live-stats" >
< h3 > Live Statistics </ h3 >
< p > Online Users : { stats . onlineUsers }</ p >
< p > Total Requests : { stats . totalRequests }</ p >
</ div >
);
}
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
Optimize Data Flow
Pass data from server to client components efficiently
Bundle Analysis Monitor bundle size regularly Use dynamic imports for large components Split client components appropriately
Hydration Optimization Minimize hydration mismatches Use proper loading states Optimize client component rendering
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
From Create React App
Identify Client Components
Find components that use hooks, event handlers, or browser APIs
Add 'use client' Directive
Add the directive to components that need client-side features
Convert Server Components
Remove ‘use client’ from components that can be server components
Optimize Data Fetching
Move data fetching to server components where possible
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.