Frontend Development Guide for Next.js #
Component Architecture #
Understanding Components #
Components are the building blocks of your React application. They should be:
- Small and focused on a single responsibility
- Reusable across different parts of your application
- Easy to test and maintain
Here’s an example of a well-structured component:
// components/ui/Card.tsx
// Define the shape of props(parameters) that this component accepts
interface CardProps {
title: string // Required: The card's title
children: React.ReactNode // Required: The content inside the card
className?: string // Optional: Additional CSS classes
}
// Card component that renders a styled container with a title and content
export function Card({
title,
children,
className = '' // Default to empty string if no className provided
}: CardProps) {
return (
// Main container with base styles and optional custom classes
<div className={`
rounded-lg /* Rounded corners */
shadow-md /* Subtle shadow for depth */
p-4 /* Padding on all sides */
${className} /* Custom classes passed as prop */
`}>
{/* Title section with consistent styling */}
<h2 className="text-xl font-bold mb-2">{title}</h2>
{/* Content section that renders the children */}
<div>{children}</div>
</div>
)
}
This Card component demonstrates several best practices:
- Clear interface definition
- Props with TypeScript types
- Default values for optional props
- Composition using children prop
- Flexible styling with className prop
Component Organization #
Organize your components into categories:
ui/
: Basic building blocks (buttons, cards, inputs)features/
: Business-specific components (UserProfile, OrderForm)layout/
: Page structure components (Header, Footer, Sidebar)
Page Structure and Routing #
Next.js 13+ uses the App Router, which provides:
- File-based routing
- Nested layouts
- Server and Client Components
- Loading and error states
Here’s a basic page structure:
// app/posts/page.tsx
import { Suspense } from 'react'
import { PostList } from '@/components/features/PostList'
import { LoadingSpinner } from '@/components/ui/LoadingSpinner'
// Posts page component - demonstrates Next.js 13+ app directory structure
export default function PostsPage() {
return (
// Container with responsive padding and max-width
<div className="
container /* Set max-width and center content */
mx-auto /* Center horizontally */
px-4 /* Horizontal padding */
py-8 /* Vertical padding */
">
{/* Page title with consistent styling */}
<h1 className="
text-3xl /* Large text size */
font-bold /* Bold weight */
mb-6 /* Bottom margin */
">
Posts
</h1>
{/*
Suspense boundary for loading state
- Shows LoadingSpinner while PostList is loading
- Part of React's concurrent features
*/}
<Suspense fallback={<LoadingSpinner />}>
<PostList />
</Suspense>
</div>
)
}
Key concepts to remember:
- Use Suspense for loading states
- Keep pages simple and focused on layout
- Move complex logic to components
State Management #
Choose the right state management approach based on your needs:
Local State (useState):
- For component-specific state
- When state doesn’t need to be shared
Context API:
- For state shared across many components
- Theme, user preferences, authentication state
Here’s a practical theme context example:
// contexts/ThemeContext.tsx
import { createContext, useContext, useState } from 'react'
// Define the shape of our context state and methods
interface ThemeContextType {
theme: 'light' | 'dark' // Current theme
toggleTheme: () => void // Function to switch themes
}
// Create the context with undefined as initial value
// We use undefined initially so TypeScript can warn us if we forget the Provider
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
// Provider component that wraps our app and makes theme available to any child component
export function ThemeProvider({
children
}: {
children: React.ReactNode // Components that will have access to the theme
}) {
// Manage the theme state
const [theme, setTheme] = useState<'light' | 'dark'>('light')
// Function to toggle between light and dark themes
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
// Provide the theme context to children
return (
<ThemeContext.Provider
value={{
theme, // Current theme state
toggleTheme // Function to change theme
}}
>
{children}
</ThemeContext.Provider>
)
}
// Custom hook to use the theme context
export function useTheme() {
// Get the context value
const context = useContext(ThemeContext)
// Throw an error if used outside of provider
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
/* Usage example:
function MyComponent() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
)
}
*/
Forms and User Input #
Forms are a crucial part of web applications. Important considerations:
- Input validation
- Error handling
- Accessibility
- User feedback
- Type safety
Use a custom form hook for consistent form handling:
// hooks/useForm.ts
import { useState, ChangeEvent, FormEvent } from 'react'
// Define the options that our hook accepts
interface UseFormOptions<T> {
initialValues: T // Initial form values
onSubmit: (values: T) => Promise<void> // Submit handler
validate?: (values: T) => Partial<Record<keyof T, string>> // Optional validation
}
// Custom hook for form handling with TypeScript generics
export function useForm<T extends Record<string, any>>({
initialValues,
onSubmit,
validate
}: UseFormOptions<T>) {
// State management
const [values, setValues] = useState<T>(initialValues) // Form values
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}) // Validation errors
const [submitting, setSubmitting] = useState(false) // Submit state
// Handle input changes
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setValues(prev => ({
...prev,
[name]: value
}))
}
// Handle form submission
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
// Validate if validation function is provided
if (validate) {
const validationErrors = validate(values)
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors)
return
}
}
// Submit the form
setSubmitting(true)
try {
await onSubmit(values)
} finally {
setSubmitting(false)
}
}
return {
values, // Current form values
errors, // Validation errors
submitting, // Whether form is submitting
handleChange, // Input change handler
handleSubmit // Form submit handler
}
}
/* Usage example:
const form = useForm({
initialValues: { email: '', password: '' },
validate: (values) => {
const errors: Record<string, string> = {}
if (!values.email) errors.email = 'Required'
if (!values.password) errors.password = 'Required'
return errors
},
onSubmit: async (values) => {
await submitToAPI(values)
}
})
*/
Best Practices #
Component Design: #
- Keep components focused and small
- Use TypeScript interfaces for props
- Handle loading and error states
- Make components reusable
State Management: #
- Keep state close to where it’s used
- Use context sparingly
- Handle loading/error states