π‘ Key Takeaways
π§© Server vs Client: Server components run on the server by default; client ones require 'use client'.
βοΈ Data Flow: Pass serializable props to server components; manage state in client ones.
π Best Practice: Use server components whenever possible β theyβre faster and lighter.
π§ Server Components
- Default rendering mode in the App Router
- Can use async/await and fetch directly
- Great for data fetching and SSR
// app/components/ServerComponent.js
export default async function ServerComponent() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
β‘ Client Components
- Must start with
'use client'at the top - Can use React hooks like
useStateanduseEffect - Runs only in the browser, not on the server
'use client';
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
useEffect(() => console.log('Mounted!'), []);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
π§± Component Boundaries
- Server components can wrap client components
- Client components cannot import server ones
- Mix them strategically for performance
import ServerComp from './ServerComponent';
import ClientComp from './ClientComponent';
export default function Page() {
return (
<>
<ServerComp />
<ClientComp />
<ServerComp><ClientComp /></ServerComp>
</>
);
}
ποΈ Layout Components
- Use to define app-wide structure
- Combine headers, footers, and main content
- Wrap with a root layout for consistency
export default function Layout({ children }) {
return (
<div className="min-h-screen flex flex-col">
<header>Header</header>
<main className="flex-1">{children}</main>
<footer>Footer</footer>
</div>
);
}
π¦ Container Components
- Reusable wrapper for layout consistency
- Keeps content centered and responsive
- Accepts children and optional class names
export default function Container({ children, className = '' }) {
return <div className={`max-w-6xl mx-auto px-4 ${className}`}>{children}</div>;
}
π Higher-Order Components
- Wrap logic around base components
- Useful for authentication or role gating
- Return enhanced versions of components
'use client';
import { useSession } from 'next-auth/react';
export const withAuth = (Comp) => (props) => {
const { data: session } = useSession();
if (!session) return <p>Access denied</p>;
return <Comp {...props} session={session} />;
};
π Props & Data Flow
- Pass serializable props to server components
- Client components can accept functions and hooks
- Combine props and children for flexible UI
export default function UserCard({ user, showEmail }) {
return (
<div>
<h3>{user.name}</h3>
{showEmail && <p>{user.email}</p>}
</div>
);
}
π§© Rendering Strategies
- Static = rendered at build time
- Dynamic = rendered per request
- Streaming = rendered progressively with Suspense
export const dynamic = 'force-dynamic';
export default async function DynamicComponent() {
const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
const data = await res.json();
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
βοΈ Performance Optimizations
- Memoize expensive renders with
React.memo() - Lazy-load heavy components with
dynamic() - Use Suspense for partial hydration
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./HeavyChart'), { ssr: false });
export default function LazyComp() {
return <Chart />;
}
π§ Error Handling
- Wrap client code in an error boundary
- Use fallback UI to recover gracefully
- Log errors in
componentDidCatch
'use client';
import { Component } from 'react';
export class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
return this.state.hasError ? <p>Something broke!</p> : this.props.children;
}
}
π Form Components
- Use server actions for secure submissions
- Use
useFormStatefor inline feedback - Client forms handle interactivity and validation
'use client';
import { useState } from 'react';
export default function ClientForm() {
const [form, setForm] = useState({ name: '', email: '' });
const handleSubmit = (e) => { e.preventDefault(); alert('Submitted!'); };
return (
<form onSubmit={handleSubmit}>
<input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })}/>
<input value={form.email} onChange={e => setForm({ ...form, email: e.target.value })}/>
<button type="submit">Send</button>
</form>
);
}
π§ Best Practices
- Keep components small and focused
- Use server components by default
- Add
'use client'only when necessary
// β
Good pattern
app/
βββ components/
β βββ ui/ # Shared UI
β βββ layout/ # Page structure
β βββ forms/ # Form logic