Chapter 5: Components and Layouts in Next.js
What are Components and Layouts?
Components are reusable pieces of UI that encapsulate functionality and styling. In Next.js, components work seamlessly with React's component model while providing additional features like automatic code splitting and server-side rendering.
Key Concepts:
- Functional Components: Modern React components using hooks
- Layout Components: Shared UI structure across multiple pages
- Page Components: Specific components for individual routes
- Reusable Components: Modular UI elements used throughout the app
- Component Composition: Building complex UIs from simple components
Why Use Components and Layouts?
Benefits:
- Reusability: Write once, use everywhere
- Maintainability: Changes in one place affect the entire app
- Performance: Automatic code splitting and optimization
- Developer Experience: Clear separation of concerns
- Testing: Easier to test individual components
- Collaboration: Team members can work on different components
How to Build Components in Next.js
1. Basic Component Structure
Simple Functional Component:
// components/Button.js
export default function Button({ children, onClick, variant = 'primary' }) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
)
}
Component with Props Validation:
// components/UserCard.js
import PropTypes from 'prop-types'
export default function UserCard({ user, showEmail = false }) {
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
{showEmail && <p>{user.email}</p>}
</div>
)
}
UserCard.propTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
avatar: PropTypes.string.isRequired,
}).isRequired,
showEmail: PropTypes.bool,
}
2. Component with State and Effects
// components/Counter.js
import { useState, useEffect } from 'react'
export default function Counter({ initialValue = 0, step = 1 }) {
const [count, setCount] = useState(initialValue)
useEffect(() => {
document.title = `Count: ${count}`
}, [count])
const increment = () => setCount(prev => prev + step)
const decrement = () => setCount(prev => prev - step)
const reset = () => setCount(initialValue)
return (
<div className="counter">
<h2>Count: {count}</h2>
<div className="controls">
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+</button>
</div>
</div>
)
}
3. Custom Hooks
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react'
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
return initialValue
}
})
const setValue = (value) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error('Error saving to localStorage:', error)
}
}
return [storedValue, setValue]
}
// Usage in component
// components/ThemeToggle.js
import { useLocalStorage } from '../hooks/useLocalStorage'
export default function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
)
}
Layout Components
1. Basic Layout Structure
// components/Layout.js
import Head from 'next/head'
import Header from './Header'
import Footer from './Footer'
export default function Layout({ children, title = 'My App' }) {
return (
<>
<Head>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<div className="layout">
<Header />
<main className="main-content">
{children}
</main>
<Footer />
</div>
</>
)
}
2. Conditional Layouts
// components/ConditionalLayout.js
import { useRouter } from 'next/router'
import DefaultLayout from './DefaultLayout'
import AdminLayout from './AdminLayout'
import AuthLayout from './AuthLayout'
export default function ConditionalLayout({ children }) {
const router = useRouter()
if (router.pathname.startsWith('/admin')) {
return <AdminLayout>{children}</AdminLayout>
}
if (router.pathname.startsWith('/auth')) {
return <AuthLayout>{children}</AuthLayout>
}
return <DefaultLayout>{children}</DefaultLayout>
}
3. Nested Layouts
// components/AdminLayout.js
import { useState } from 'react'
import Sidebar from './Sidebar'
import TopBar from './TopBar'
export default function AdminLayout({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<div className="admin-layout">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="admin-content">
<TopBar onMenuClick={() => setSidebarOpen(true)} />
<main className="admin-main">
{children}
</main>
</div>
</div>
)
}
Using Layouts in Pages
1. Layout per Page
// pages/about.js
import Layout from '../components/Layout'
export default function About() {
return (
<Layout title="About Us">
<h1>About Our Company</h1>
<p>We are a leading technology company...</p>
</Layout>
)
}
2. Global Layout with _app.js
// pages/_app.js
import Layout from '../components/Layout'
import '../styles/globals.css'
export default function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
3. Layout with getLayout Pattern
// pages/dashboard.js
import Layout from '../components/Layout'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<p>Welcome to your dashboard!</p>
</div>
)
}
Dashboard.getLayout = function getLayout(page) {
return <Layout title="Dashboard">{page}</Layout>
}
Advanced Component Patterns
1. Higher-Order Components (HOCs)
// components/withAuth.js
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const router = useRouter()
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [loading, setLoading] = useState(true)
useEffect(() => {
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/verify')
if (response.ok) {
setIsAuthenticated(true)
} else {
router.push('/login')
}
} catch (error) {
router.push('/login')
} finally {
setLoading(false)
}
}
checkAuth()
}, [router])
if (loading) return <div>Loading...</div>
if (!isAuthenticated) return null
return <WrappedComponent {...props} />
}
}
// Usage
// pages/protected.js
import { withAuth } from '../components/withAuth'
function ProtectedPage() {
return <h1>This is a protected page</h1>
}
export default withAuth(ProtectedPage)
2. Render Props Pattern
// components/DataFetcher.js
import { useState, useEffect } from 'react'
export default function DataFetcher({ url, children }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url)
const result = await response.json()
setData(result)
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return children({ data, loading, error })
}
// Usage
// components/UserList.js
import DataFetcher from './DataFetcher'
export default function UserList() {
return (
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading users...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}}
</DataFetcher>
)
}
3. Compound Components
// components/Modal.js
import { createContext, useContext } from 'react'
const ModalContext = createContext()
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null
return (
<ModalContext.Provider value={{ onClose }}>
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
</ModalContext.Provider>
)
}
function ModalHeader({ children }) {
return <div className="modal-header">{children}</div>
}
function ModalBody({ children }) {
return <div className="modal-body">{children}</div>
}
function ModalFooter({ children }) {
return <div className="modal-footer">{children}</div>
}
function ModalCloseButton() {
const { onClose } = useContext(ModalContext)
return (
<button className="modal-close" onClick={onClose}>
×
</button>
)
}
Modal.Header = ModalHeader
Modal.Body = ModalBody
Modal.Footer = ModalFooter
Modal.CloseButton = ModalCloseButton
export default Modal
// Usage
// components/UserModal.js
import Modal from './Modal'
export default function UserModal({ user, isOpen, onClose }) {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>
<h2>User Details</h2>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</Modal.Body>
<Modal.Footer>
<button onClick={onClose}>Close</button>
</Modal.Footer>
</Modal>
)
}
Component Styling
1. CSS Modules
/* components/Button.module.css */
.button {
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.primary {
background-color: #0070f3;
color: white;
}
.primary:hover {
background-color: #0051cc;
}
.secondary {
background-color: #f0f0f0;
color: #333;
}
.secondary:hover {
background-color: #e0e0e0;
}
// components/Button.js
import styles from './Button.module.css'
export default function Button({ children, variant = 'primary', ...props }) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
{...props}
>
{children}
</button>
)
}
2. Styled JSX
// components/StyledComponent.js
export default function StyledComponent() {
return (
<div>
<h1>Styled Component</h1>
<style jsx>{`
h1 {
color: #0070f3;
font-size: 2rem;
}
div {
padding: 2rem;
background: #f5f5f5;
}
`}</style>
</div>
)
}
Best Practices
1. Component Organization
// ✅ Good: Clear component structure
components/
├── ui/ # Reusable UI components
│ ├── Button/
│ │ ├── Button.js
│ │ ├── Button.module.css
│ │ └── index.js
│ └── Modal/
├── forms/ # Form components
├── layout/ # Layout components
└── features/ # Feature-specific components
├── auth/
└── dashboard/
2. Props Interface
// ✅ Good: Clear prop interface
export default function UserCard({
user,
showEmail = false,
onEdit = () => {},
className = ''
}) {
return (
<div className={`user-card ${className}`}>
{/* Component content */}
</div>
)
}
3. Error Boundaries
// components/ErrorBoundary.js
import { Component } from 'react'
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>
}
return this.props.children
}
}
export default ErrorBoundary
Common Mistakes to Avoid
1. Inline Functions in JSX
// ❌ Wrong: Creates new function on every render
<button onClick={() => handleClick(id)}>Click me</button>
// ✅ Correct: Use useCallback or define outside render
const handleClick = useCallback((id) => {
// handle click
}, [])
<button onClick={() => handleClick(id)}>Click me</button>
2. Missing Key Props
// ❌ Wrong: Missing key prop
{users.map(user => <UserCard user={user} />)}
// ✅ Correct: Include key prop
{users.map(user => <UserCard key={user.id} user={user} />)}
3. Direct State Mutation
// ❌ Wrong: Direct mutation
const [users, setUsers] = useState([])
users.push(newUser)
setUsers(users)
// ✅ Correct: Create new array
setUsers([...users, newUser])
Summary
Components and layouts are the building blocks of Next.js applications:
- Components provide reusable UI elements
- Layouts create consistent page structure
- Custom hooks encapsulate reusable logic
- Advanced patterns solve complex UI problems
- Proper styling enhances user experience
Key Takeaways:
- Use functional components with hooks
- Create reusable components for common UI elements
- Implement layouts for consistent page structure
- Follow component composition patterns
- Use proper error handling and performance optimization
This tutorial is part of the comprehensive Next.js learning path at syscook.dev. Continue to the next chapter to learn about styling and CSS in Next.js.
Author: syscook.dev
Last Updated: December 2024