Skip to main content

Learning Objectives

By the end of this lesson, you will be able to:
  • Implement advanced component patterns and composition techniques
  • Apply performance optimization strategies for production applications
  • Create robust error handling and resilience patterns
  • Set up comprehensive testing strategies for Next.js applications
  • Prepare applications for production deployment
Duration: 3-4 hours

Part 1: Strategic Understanding

Advanced Patterns Overview

Now that you’ve mastered the fundamentals of Next.js App Router migration, let’s explore advanced patterns and best practices that will make your applications production-ready and performant.

Overview

Next.js App Router provides built-in support for loading states through the loading.tsx file. This feature helps create better user experiences by handling loading states gracefully.

Loading UI

The loading.tsx file creates a loading UI that shows while a route segment is loading.

Key Benefits

Loading Benefits

  • Instant loading feedback
  • Better perceived performance
  • Prevents layout shift
  • Automatic implementation

Loading Scope

  • Applies to route segment
  • Wraps page and children
  • Automatic activation
  • Nested loading support

Basic Loading Component

// app/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
    </div>
  );
}

Simple Skeleton Loading

// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="mb-8">
        <div className="h-8 bg-gray-200 rounded w-1/3 animate-pulse"></div>
        <div className="h-4 bg-gray-200 rounded w-1/2 mt-2 animate-pulse"></div>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="bg-white p-6 rounded-lg shadow-md">
            <div className="h-4 bg-gray-200 rounded w-24 mb-2 animate-pulse"></div>
            <div className="h-8 bg-gray-200 rounded w-16 animate-pulse"></div>
          </div>
        ))}
      </div>

      <div className="bg-white rounded-lg shadow-md p-8">
        <div className="space-y-4">
          {Array.from({ length: 5 }).map((_, i) => (
            <div
              key={i}
              className="h-4 bg-gray-200 rounded animate-pulse"></div>
          ))}
        </div>
      </div>
    </div>
  );
}

Nested Loading States

app/
├── loading.tsx              # Root loading
├── dashboard/
│   ├── loading.tsx          # Dashboard loading
│   ├── page.tsx
│   └── settings/
│       ├── loading.tsx      # Settings loading
│       └── page.tsx
Each loading component applies to its route segment and children.

Basic Loading Patterns

Page-Level Loading

// app/blog/loading.tsx
export default function BlogLoading() {
  return (
    <div className="blog-loading">
      <div className="loading-header">
        <div className="h-8 bg-gray-200 rounded w-1/3 animate-pulse"></div>
      </div>

      <div className="loading-content">
        <div className="space-y-4">
          {Array.from({ length: 3 }).map((_, i) => (
            <div
              key={i}
              className="h-32 bg-gray-200 rounded animate-pulse"></div>
          ))}
        </div>
      </div>
    </div>
  );
}

Simple Card Loading

// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="products-loading">
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="bg-white p-6 rounded-lg shadow-md">
            <div className="h-48 bg-gray-200 rounded mb-4 animate-pulse"></div>
            <div className="h-4 bg-gray-200 rounded w-3/4 mb-2 animate-pulse"></div>
            <div className="h-4 bg-gray-200 rounded w-1/2 animate-pulse"></div>
          </div>
        ))}
      </div>
    </div>
  );
}

Best Practices for Module 1

Loading State Design

  1. Match Layout Structure: Create loading states that match your page layout
  2. Use Skeleton Screens: Implement skeleton loading for better perceived performance
  3. Provide Context: Show what’s loading and why it might take time
  4. Keep It Simple: Start with basic loading patterns and enhance later

File Organization

app/
├── loading.tsx              # Global loading
├── dashboard/
│   ├── loading.tsx          # Dashboard-specific loading
│   └── page.tsx
└── blog/
    ├── loading.tsx          # Blog-specific loading
    └── page.tsx
Key Takeaway: Loading states are essential for creating good user experiences. Use loading.tsx files to provide immediate feedback while pages are loading, and match the loading UI to your page structure.

What You’ll Learn

Advanced Patterns

  • Compound component patterns - Render props and HOCs - Custom hooks for shared logic - Advanced composition techniques

Performance Optimization

  • Bundle optimization strategies - Lazy loading patterns - Caching strategies - Performance monitoring and profiling

Production Readiness

  • Error boundaries and resilience - Testing strategies - Security best practices - Deployment preparation

Enterprise Patterns

  • Scalable architecture patterns - Code organization - Documentation standards - Team collaboration workflows

Part 2: Hands-On Implementation

Exercise 1: Advanced Component Patterns

Objective: Implement compound component patterns and advanced composition Requirements:
  • Create a compound DataTable component with flexible composition
  • Implement render prop pattern for data fetching
  • Build custom hooks for shared component logic
  • Add proper TypeScript generics for type safety
Deliverables:
  • Compound DataTable component with sub-components
  • Custom hooks for data management
  • Render prop components for flexibility
  • Comprehensive TypeScript interfaces

Exercise 2: Performance Optimization

Objective: Optimize application performance for production Requirements:
  • Implement dynamic imports for code splitting
  • Add proper caching strategies
  • Optimize bundle size and loading performance
  • Set up performance monitoring
Deliverables:
  • Dynamic import implementations
  • Caching configuration
  • Bundle analysis and optimization
  • Performance monitoring setup

Exercise 3: Error Handling & Resilience

Objective: Create robust error handling patterns Requirements:
  • Implement comprehensive error boundaries
  • Add retry mechanisms for failed operations
  • Create fallback UI components
  • Set up error logging and monitoring
Deliverables:
  • Error boundary components
  • Retry logic implementations
  • Fallback UI components
  • Error logging system

Exercise 4: Testing Strategy

Objective: Set up comprehensive testing for Next.js applications Requirements:
  • Configure Jest and React Testing Library
  • Write unit tests for components
  • Create integration tests for routing
  • Set up end-to-end testing
Deliverables:
  • Testing configuration
  • Unit test suites
  • Integration tests
  • E2E test setup

Exercise 5: Production Deployment

Objective: Prepare application for production deployment Requirements:
  • Configure production build optimizations
  • Set up environment variables
  • Create deployment scripts
  • Add health checks and monitoring
Deliverables:
  • Production build configuration
  • Environment setup
  • Deployment documentation
  • Monitoring configuration

Advanced Pattern Implementation Examples

Compound Component Pattern

// components/ui/DataTable.tsx
import React, { createContext, useContext } from "react";

interface DataTableContextType {
  data: any[];
  columns: any[];
  loading: boolean;
}

const DataTableContext = createContext<DataTableContextType | null>(null);

const useDataTable = () => {
  const context = useContext(DataTableContext);
  if (!context) {
    throw new Error("useDataTable must be used within DataTable");
  }
  return context;
};

interface DataTableProps {
  data: any[];
  columns: any[];
  loading?: boolean;
  children: React.ReactNode;
}

export function DataTable({
  data,
  columns,
  loading = false,
  children,
}: DataTableProps) {
  return (
    <DataTableContext.Provider value={{ data, columns, loading }}>
      <div className="overflow-x-auto">{children}</div>
    </DataTableContext.Provider>
  );
}

DataTable.Header = function DataTableHeader() {
  const { columns } = useDataTable();
  return (
    <thead className="bg-gray-50">
      <tr>
        {columns.map((column, index) => (
          <th
            key={index}
            className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
            {column.header}
          </th>
        ))}
      </tr>
    </thead>
  );
};

DataTable.Body = function DataTableBody() {
  const { data, columns, loading } = useDataTable();

  if (loading) {
    return (
      <tbody>
        <tr>
          <td colSpan={columns.length} className="px-6 py-4 text-center">
            Loading...
          </td>
        </tr>
      </tbody>
    );
  }

  return (
    <tbody className="bg-white divide-y divide-gray-200">
      {data.map((row, index) => (
        <tr key={index} className="hover:bg-gray-50">
          {columns.map((column, colIndex) => (
            <td
              key={colIndex}
              className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
              {column.render
                ? column.render(row[column.key], row)
                : row[column.key]}
            </td>
          ))}
        </tr>
      ))}
    </tbody>
  );
};

// Usage:
<DataTable data={customers} columns={columns} loading={isLoading}>
  <DataTable.Header />
  <DataTable.Body />
</DataTable>;

Custom Hook for Data Management

// hooks/useDataTable.ts
import { useState, useEffect, useCallback } from "react";

interface UseDataTableOptions {
  initialData?: any[];
  fetchData?: () => Promise<any[]>;
  pageSize?: number;
}

export function useDataTable({
  initialData = [],
  fetchData,
  pageSize = 10,
}: UseDataTableOptions) {
  const [data, setData] = useState(initialData);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [currentPage, setCurrentPage] = useState(1);

  const loadData = useCallback(async () => {
    if (!fetchData) return;

    setLoading(true);
    setError(null);

    try {
      const result = await fetchData();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : "An error occurred");
    } finally {
      setLoading(false);
    }
  }, [fetchData]);

  useEffect(() => {
    loadData();
  }, [loadData]);

  const paginatedData = data.slice(
    (currentPage - 1) * pageSize,
    currentPage * pageSize
  );

  return {
    data: paginatedData,
    loading,
    error,
    currentPage,
    totalPages: Math.ceil(data.length / pageSize),
    setCurrentPage,
    refresh: loadData,
  };
}

Error Boundary with Retry Logic

// components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  onRetry?: () => void;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error("ErrorBoundary caught an error:", error, errorInfo);
    // Log to error reporting service
  }

  handleRetry = () => {
    this.setState({ hasError: false, error: undefined });
    this.props.onRetry?.();
  };

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div className="flex flex-col items-center justify-center p-8">
            <h2 className="text-xl font-semibold text-red-600 mb-4">
              Something went wrong
            </h2>
            <p className="text-gray-600 mb-4">
              {this.state.error?.message || "An unexpected error occurred"}
            </p>
            <button
              onClick={this.handleRetry}
              className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
              Try Again
            </button>
          </div>
        )
      );
    }

    return this.props.children;
  }
}

Part 3: Reflection & Extension

Performance Optimization Best Practices

Bundle Analysis and Optimization

// next.config.js
const nextConfig = {
  experimental: {
    optimizeCss: true,
    optimizePackageImports: ["@mui/material", "@mui/icons-material"],
  },
  webpack: (config, { dev, isServer }) => {
    if (!dev && !isServer) {
      config.optimization.splitChunks.cacheGroups = {
        ...config.optimization.splitChunks.cacheGroups,
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          chunks: "all",
        },
      };
    }
    return config;
  },
};

module.exports = nextConfig;

Dynamic Imports for Code Splitting

// components/LazyComponent.tsx
import dynamic from "next/dynamic";
import { Suspense } from "react";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <div className="animate-pulse bg-gray-200 h-64 rounded" />,
  ssr: false,
});

const LazyComponent = () => {
  return (
    <Suspense fallback={<div>Loading chart...</div>}>
      <HeavyChart />
    </Suspense>
  );
};

Testing Strategy Implementation

Jest Configuration

// jest.config.js
const nextJest = require("next/jest");

const createJestConfig = nextJest({
  dir: "./",
});

const customJestConfig = {
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
  moduleNameMapping: {
    "^@/(.*)$": "<rootDir>/$1",
  },
  testEnvironment: "jest-environment-jsdom",
};

module.exports = createJestConfig(customJestConfig);

Component Testing Example

// __tests__/components/DataTable.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { DataTable } from "@/components/ui/DataTable";

const mockData = [
  { id: 1, name: "John Doe", email: "john@example.com" },
  { id: 2, name: "Jane Smith", email: "jane@example.com" },
];

const mockColumns = [
  { key: "name", header: "Name" },
  { key: "email", header: "Email" },
];

describe("DataTable", () => {
  it("renders data correctly", () => {
    render(
      <DataTable data={mockData} columns={mockColumns}>
        <DataTable.Header />
        <DataTable.Body />
      </DataTable>
    );

    expect(screen.getByText("John Doe")).toBeInTheDocument();
    expect(screen.getByText("jane@example.com")).toBeInTheDocument();
  });

  it("shows loading state", () => {
    render(
      <DataTable data={[]} columns={mockColumns} loading={true}>
        <DataTable.Header />
        <DataTable.Body />
      </DataTable>
    );

    expect(screen.getByText("Loading...")).toBeInTheDocument();
  });
});

Reflection Questions

  1. How do advanced component patterns improve code maintainability and reusability?
    • Consider compound components vs traditional props
    • Think about custom hooks for shared logic
    • Reflect on render props and composition patterns
  2. What performance optimization strategies had the biggest impact on your application?
    • Bundle splitting and lazy loading
    • Caching strategies
    • Component optimization techniques
  3. How does comprehensive error handling improve user experience?
    • Error boundaries and fallback UI
    • Retry mechanisms
    • User-friendly error messages

Extension Activities

  1. Advanced Architecture Patterns:
    • Research micro-frontend architectures
    • Study state management patterns for large applications
    • Explore server-side rendering optimization
  2. Production Monitoring:
    • Set up application performance monitoring (APM)
    • Implement error tracking and logging
    • Create performance dashboards
  3. Security Best Practices:
    • Implement content security policies
    • Add authentication and authorization patterns
    • Set up security headers and validation

Production Deployment Checklist

Before deploying to production, ensure you have:
  • Implemented comprehensive error boundaries and fallback UI - [ ] Set up performance monitoring and error tracking - [ ] Configured proper caching strategies - [ ] Optimized bundle size and loading performance - [ ] Added comprehensive testing coverage - [ ] Implemented security best practices - [ ] Set up proper environment configuration - [ ] Created deployment documentation
  • Configured health checks and monitoring - [ ] Tested in staging environment

Key Takeaways

By completing this lesson, you’ve learned:
  • Advanced Component Patterns: Compound components, render props, and custom hooks
  • Performance Optimization: Bundle splitting, lazy loading, and caching strategies
  • Error Handling: Robust error boundaries and retry mechanisms
  • Testing Strategies: Unit, integration, and end-to-end testing approaches
  • Production Readiness: Deployment preparation and monitoring setup
These advanced patterns and best practices will help you build scalable, maintainable, and production-ready Next.js applications.

Next Steps

After completing this lesson, you’ll be ready to move on to Module 2: Data Integration & API Modernization, where you’ll:
  • Modernize the VSL-Api backend
  • Implement API routes and server actions
  • Integrate with databases
  • Set up authentication systems
Key Takeaway: Advanced patterns and best practices are essential for building production-ready applications. Focus on creating scalable, maintainable code that follows enterprise-level standards and provides excellent user experience.

Support and Resources

If you need help with advanced patterns:
  • Review the Next.js documentation for advanced features
  • Check the React documentation for component patterns
  • Explore performance optimization guides
  • Join the Next.js community for best practices
  • Study open-source Next.js applications for real-world examples
Congratulations on completing Module 1! 🎉