Frontend Essentials

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:

  1. Local State (useState):

    • For component-specific state
    • When state doesn’t need to be shared
  2. 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