Strategic Software Engineer and Business Problem Solver - Merveille Alexandre
tutorialsadvanced

Advanced React Architecture and Patterns

Master advanced React patterns, state management, testing strategies, and enterprise-scale architecture.

January 19, 2026
30 min read
Merveille Alexander

Advanced React Architecture and Patterns

Master advanced React patterns, state management, testing strategies, and enterprise-scale architecture


Before We Begin: What This Tutorial Is (And Isn't)

This is a tutorial, not just an article. By the end, you'll have built a production-ready application architecture from scratch. We'll write code together, make mistakes together, and fix them together.

What you'll learn:

  • Core concepts and practical applications
  • Step-by-step implementation guide
  • Best practices and optimization tips
  • Real-world tradeoffs and decision-making

Prerequisites:

  • You've built at least one React app before
  • You know hooks, context, and basic TypeScript
  • You're comfortable with the command line

Time to complete: About 2-3 hours, depending on your pace

Let's build something we can actually use.


Part 1: The Problem We're Solving

Every React tutorial shows you how to build a todo app. But real applications aren't todo apps. They have complex state, dozens of developers, performance requirements, and code that needs to last for years.

The Scenario

You're leading a team building a customer support dashboard. Requirements:

  • Real-time ticket updates (WebSocket)
  • Complex filtering and sorting
  • Multiple user roles with different permissions
  • Offline support with sync
  • Analytics tracking
  • Ten different teams contributing code
  • Must ship in 8 weeks

A simple component hierarchy won't cut it. You need architecture.

What We're Building Today

We'll build a simplified version of that dashboard together, focusing on the architectural patterns that make it maintainable and scalable.

Code
Customer Support Dashboard
β”œβ”€β”€ Ticket list with real-time updates
β”œβ”€β”€ Filtering and search
β”œβ”€β”€ User permissions
β”œβ”€β”€ Offline queue
└── Analytics tracking

Let's start from scratch and build it right.


Part 2: Setting Up the Foundation

First, let's create our project. I'll use Next.js because it's the standard in 2026, but the patterns we'll learn apply to any React setup.

Code
# Create the project
npx create-next-app@latest support-dashboard --typescript --tailwind --app

# Navigate in
cd support-dashboard

# Install additional dependencies
npm install zustand react-query socket.io-client zod
npm install -D @types/node @testing-library/react @testing-library/jest-dom

The Folder Structure

Before writing code, let's establish our architecture. We're using Feature-Sliced Design, which organizes by domain, not by file type.

Code
src/
β”œβ”€β”€ app/                    # Next.js App Router (routes only)
β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”œβ”€β”€ page.tsx
β”‚   └── tickets/
β”‚       β”œβ”€β”€ page.tsx        # /tickets
β”‚       └── [id]/
β”‚           └── page.tsx    # /tickets/123
β”‚
β”œβ”€β”€ features/               # Business features
β”‚   β”œβ”€β”€ tickets/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   └── ...
β”‚   └── analytics/
β”‚       └── ...
β”‚
β”œβ”€β”€ entities/               # Core business models
β”‚   β”œβ”€β”€ ticket/
β”‚   β”‚   β”œβ”€β”€ types.ts
β”‚   β”‚   └── constants.ts
β”‚   └── user/
β”‚       └── types.ts
β”‚
β”œβ”€β”€ shared/                 # Reusable, non-domain code
β”‚   β”œβ”€β”€ ui/                 # Design system components
β”‚   β”œβ”€β”€ lib/                # Utilities
β”‚   └── api/                # Generic API clients
β”‚
└── widgets/                # Composite components
    └── ticket-table/
        └── index.tsx

Create these folders now:

Code
mkdir -p src/{features/{tickets,auth,analytics},entities/{ticket,user},shared/{ui,lib,api},widgets}

This structure might feel like overkill for a small app. But for a production app that will grow, it's perfect. Each feature is isolated, shared code is explicit, and you can delete entire features without leaving orphaned components.


Part 3: Core Domain Models (Entities)

Every application has core business concepts. In our case: Tickets and Users. These are our entities.

Step 1: Define Ticket Types

Code
// src/entities/ticket/types.ts

export type TicketStatus = 'open' | 'in-progress' | 'resolved' | 'closed';
export type TicketPriority = 'low' | 'medium' | 'high' | 'critical';

export interface Ticket {
  id: string;
  title: string;
  description: string;
  status: TicketStatus;
  priority: TicketPriority;
  createdAt: Date;
  updatedAt: Date;
  assignedToId: string | null;
  createdById: string;
  tags: string[];
}

export interface TicketComment {
  id: string;
  ticketId: string;
  content: string;
  createdById: string;
  createdAt: Date;
}

export interface TicketFilters {
  status?: TicketStatus[];
  priority?: TicketPriority[];
  assignedToId?: string;
  search?: string;
  tags?: string[];
}

Step 2: Define User Types

Code
// src/entities/user/types.ts

export type UserRole = 'agent' | 'manager' | 'admin';

export interface User {
  id: string;
  name: string;
  email: string;
  role: UserRole;
  avatar?: string;
  department?: string;
}

export interface Session {
  user: User;
  token: string;
  expiresAt: Date;
}

Step 3: Create Constants

Code
// src/entities/ticket/constants.ts

export const TICKET_STATUSES: TicketStatus[] = ['open', 'in-progress', 'resolved', 'closed'];
export const TICKET_PRIORITIES: TicketPriority[] = ['low', 'medium', 'high', 'critical'];

export const TICKET_STATUS_COLORS: Record<TicketStatus, string> = {
  'open': 'bg-blue-100 text-blue-800',
  'in-progress': 'bg-yellow-100 text-yellow-800',
  'resolved': 'bg-green-100 text-green-800',
  'closed': 'bg-gray-100 text-gray-800'
};

export const TICKET_PRIORITY_COLORS: Record<TicketPriority, string> = {
  'low': 'bg-gray-100 text-gray-800',
  'medium': 'bg-blue-100 text-blue-800',
  'high': 'bg-orange-100 text-orange-800',
  'critical': 'bg-red-100 text-red-800'
};

Why this matters: These types are the source of truth. Every feature, every component will reference them. If a ticket's structure changes, TypeScript tells us everywhere that needs updating.


Part 4: The Ticket Feature - API Layer

Now let's build the tickets feature. We'll start with the API layer, which handles data fetching and real-time updates.

Step 1: Create API Client

Code
// src/features/tickets/api/client.ts

import { Ticket, TicketFilters, TicketComment } from '@/entities/ticket/types';

// In a real app, this would be in shared/api
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

class TicketsApiClient {
  private async fetch<T>(endpoint: string, options?: RequestInit): Promise<T> {
    const response = await fetch(`${API_BASE}/api${endpoint}`, {
      headers: {
        'Content-Type': 'application/json',
      },
      ...options,
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }

    return response.json();
  }

  async getTickets(filters?: TicketFilters): Promise<Ticket[]> {
    const params = new URLSearchParams();
    
    if (filters?.status?.length) {
      params.append('status', filters.status.join(','));
    }
    if (filters?.priority?.length) {
      params.append('priority', filters.priority.join(','));
    }
    if (filters?.assignedToId) {
      params.append('assignedTo', filters.assignedToId);
    }
    if (filters?.search) {
      params.append('search', filters.search);
    }
    
    return this.fetch(`/tickets?${params.toString()}`);
  }

  async getTicketById(id: string): Promise<Ticket> {
    return this.fetch(`/tickets/${id}`);
  }

  async createTicket(data: Partial<Ticket>): Promise<Ticket> {
    return this.fetch('/tickets', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async updateTicket(id: string, data: Partial<Ticket>): Promise<Ticket> {
    return this.fetch(`/tickets/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async getComments(ticketId: string): Promise<TicketComment[]> {
    return this.fetch(`/tickets/${ticketId}/comments`);
  }

  async addComment(ticketId: string, content: string): Promise<TicketComment> {
    return this.fetch(`/tickets/${ticketId}/comments`, {
      method: 'POST',
      body: JSON.stringify({ content }),
    });
  }
}

// Singleton export
export const ticketsApi = new TicketsApiClient();

Step 2: WebSocket Connection for Real-time Updates

Code
// src/features/tickets/api/socket.ts

import { io, Socket } from 'socket.io-client';

type TicketUpdateCallback = (ticketId: string, data: Partial<Ticket>) => void;
type NewTicketCallback = (ticket: Ticket) => void;

class TicketsSocketClient {
  private socket: Socket | null = null;
  private ticketUpdateCallbacks: Set<TicketUpdateCallback> = new Set();
  private newTicketCallbacks: Set<NewTicketCallback> = new Set();

  connect() {
    if (this.socket) return;

    this.socket = io(process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001', {
      path: '/ws/tickets',
    });

    this.socket.on('ticket:updated', (ticketId: string, data: Partial<Ticket>) => {
      this.ticketUpdateCallbacks.forEach(cb => cb(ticketId, data));
    });

    this.socket.on('ticket:created', (ticket: Ticket) => {
      this.newTicketCallbacks.forEach(cb => cb(ticket));
    });
  }

  disconnect() {
    if (this.socket) {
      this.socket.disconnect();
      this.socket = null;
    }
  }

  onTicketUpdate(callback: TicketUpdateCallback) {
    this.ticketUpdateCallbacks.add(callback);
    return () => this.ticketUpdateCallbacks.delete(callback);
  }

  onNewTicket(callback: NewTicketCallback) {
    this.newTicketCallbacks.add(callback);
    return () => this.newTicketCallbacks.delete(callback);
  }

  subscribeToTicket(ticketId: string) {
    this.socket?.emit('ticket:subscribe', ticketId);
  }

  unsubscribeFromTicket(ticketId: string) {
    this.socket?.emit('ticket:unsubscribe', ticketId);
  }
}

export const ticketsSocket = new TicketsSocketClient();

Step 3: Create Data Access Hooks

We'll use React Query (now TanStack Query) for server state management:

Code
// src/features/tickets/hooks/useTickets.ts

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ticketsApi } from '../api/client';
import { ticketsSocket } from '../api/socket';
import { TicketFilters, Ticket } from '@/entities/ticket/types';
import { useEffect } from 'react';

// Query keys for cache management
export const ticketKeys = {
  all: ['tickets'] as const,
  lists: () => [...ticketKeys.all, 'list'] as const,
  list: (filters: TicketFilters) => [...ticketKeys.lists(), filters] as const,
  details: () => [...ticketKeys.all, 'detail'] as const,
  detail: (id: string) => [...ticketKeys.details(), id] as const,
};

export function useTickets(filters?: TicketFilters) {
  const queryClient = useQueryClient();

  // Main query
  const query = useQuery({
    queryKey: ticketKeys.list(filters || {}),
    queryFn: () => ticketsApi.getTickets(filters),
    staleTime: 1000 * 60, // 1 minute
  });

  // Real-time updates
  useEffect(() => {
    ticketsSocket.connect();

    const unsubscribeUpdate = ticketsSocket.onTicketUpdate((ticketId, data) => {
      // Update the ticket in the cache
      queryClient.setQueryData<Ticket[]>(
        ticketKeys.list(filters || {}),
        (old) => old?.map(ticket => 
          ticket.id === ticketId ? { ...ticket, ...data } : ticket
        )
      );

      // Also update the individual ticket cache
      queryClient.setQueryData<Ticket>(
        ticketKeys.detail(ticketId),
        (old) => old ? { ...old, ...data } : old
      );
    });

    const unsubscribeNew = ticketsSocket.onNewTicket((ticket) => {
      // Add to list if it matches current filters
      if (matchesFilters(ticket, filters)) {
        queryClient.setQueryData<Ticket[]>(
          ticketKeys.list(filters || {}),
          (old) => old ? [ticket, ...old] : [ticket]
        );
      }
    });

    return () => {
      unsubscribeUpdate();
      unsubscribeNew();
      ticketsSocket.disconnect();
    };
  }, [filters, queryClient]);

  return query;
}

// Helper to check if ticket matches filters
function matchesFilters(ticket: Ticket, filters?: TicketFilters): boolean {
  if (!filters) return true;
  
  if (filters.status?.length && !filters.status.includes(ticket.status)) {
    return false;
  }
  if (filters.priority?.length && !filters.priority.includes(ticket.priority)) {
    return false;
  }
  if (filters.assignedToId && ticket.assignedToId !== filters.assignedToId) {
    return false;
  }
  if (filters.search) {
    const search = filters.search.toLowerCase();
    return ticket.title.toLowerCase().includes(search) || 
           ticket.description.toLowerCase().includes(search);
  }
  
  return true;
}
Code
// src/features/tickets/hooks/useTicket.ts

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ticketsApi } from '../api/client';
import { ticketsSocket } from '../api/socket';
import { ticketKeys } from './useTickets';
import { Ticket } from '@/entities/ticket/types';
import { useEffect } from 'react';

export function useTicket(id: string) {
  const queryClient = useQueryClient();

  const query = useQuery({
    queryKey: ticketKeys.detail(id),
    queryFn: () => ticketsApi.getTicketById(id),
    staleTime: 1000 * 60,
  });

  // Subscribe to real-time updates for this specific ticket
  useEffect(() => {
    if (!id) return;

    ticketsSocket.connect();
    ticketsSocket.subscribeToTicket(id);

    return () => {
      ticketsSocket.unsubscribeFromTicket(id);
    };
  }, [id]);

  const updateMutation = useMutation({
    mutationFn: (data: Partial<Ticket>) => ticketsApi.updateTicket(id, data),
    onMutate: async (newData) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ticketKeys.detail(id) });

      // Snapshot previous value
      const previousTicket = queryClient.getQueryData<Ticket>(ticketKeys.detail(id));

      // Optimistically update
      queryClient.setQueryData<Ticket>(ticketKeys.detail(id), (old) => ({
        ...old!,
        ...newData,
      }));

      return { previousTicket };
    },
    onError: (err, newData, context) => {
      // Rollback on error
      queryClient.setQueryData(ticketKeys.detail(id), context?.previousTicket);
    },
    onSettled: () => {
      // Refetch to ensure consistency
      queryClient.invalidateQueries({ queryKey: ticketKeys.detail(id) });
    },
  });

  return {
    ticket: query.data,
    isLoading: query.isLoading,
    isError: query.isError,
    updateTicket: updateMutation.mutate,
    isUpdating: updateMutation.isPending,
  };
}

Key patterns here:

  • Query keys as constants: Prevents typos and enables type safety
  • Optimistic updates: UI feels instant while server processes
  • WebSocket integration: Real-time updates flow into cache
  • Separation of concerns: API client, socket client, and hooks each do one thing

Part 5: State Management with Zustand

For client state (UI state, filters, etc.), we'll use Zustand. It's simpler than Redux but more powerful than context for complex state.

Step 1: Create Filter Store

Code
// src/features/tickets/store/filterStore.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { TicketFilters, TicketStatus, TicketPriority } from '@/entities/ticket/types';

interface FilterState {
  // State
  filters: TicketFilters;
  
  // Actions
  setStatus: (status: TicketStatus[]) => void;
  setPriority: (priority: TicketPriority[]) => void;
  setAssignedTo: (userId: string | null) => void;
  setSearch: (search: string) => void;
  setTags: (tags: string[]) => void;
  resetFilters: () => void;
  
  // Derived state
  hasActiveFilters: () => boolean;
  getActiveFilterCount: () => number;
}

export const useFilterStore = create<FilterState>()(
  persist(
    (set, get) => ({
      // Initial state
      filters: {
        status: [],
        priority: [],
        assignedToId: null,
        search: '',
        tags: [],
      },

      // Actions
      setStatus: (status) => 
        set((state) => ({
          filters: { ...state.filters, status }
        })),

      setPriority: (priority) =>
        set((state) => ({
          filters: { ...state.filters, priority }
        })),

      setAssignedTo: (assignedToId) =>
        set((state) => ({
          filters: { ...state.filters, assignedToId }
        })),

      setSearch: (search) =>
        set((state) => ({
          filters: { ...state.filters, search }
        })),

      setTags: (tags) =>
        set((state) => ({
          filters: { ...state.filters, tags }
        })),

      resetFilters: () =>
        set({
          filters: {
            status: [],
            priority: [],
            assignedToId: null,
            search: '',
            tags: [],
          }
        }),

      // Derived state
      hasActiveFilters: () => {
        const filters = get().filters;
        return (
          filters.status.length > 0 ||
          filters.priority.length > 0 ||
          filters.assignedToId !== null ||
          filters.search !== '' ||
          (filters.tags?.length || 0) > 0
        );
      },

      getActiveFilterCount: () => {
        const filters = get().filters;
        let count = 0;
        if (filters.status.length) count++;
        if (filters.priority.length) count++;
        if (filters.assignedToId) count++;
        if (filters.search) count++;
        if (filters.tags?.length) count++;
        return count;
      },
    }),
    {
      name: 'ticket-filters', // Persist to localStorage
    }
  )
);

Step 2: Create UI State Store

Code
// src/features/tickets/store/uiStore.ts

import { create } from 'zustand';

interface UIState {
  // View state
  viewMode: 'table' | 'grid';
  selectedTicketIds: string[];
  isFilterDrawerOpen: boolean;
  
  // Actions
  setViewMode: (mode: 'table' | 'grid') => void;
  toggleTicketSelection: (ticketId: string) => void;
  selectAll: (ticketIds: string[]) => void;
  clearSelection: () => void;
  toggleFilterDrawer: () => void;
  
  // Derived
  hasSelection: boolean;
  selectedCount: number;
}

export const useUIStore = create<UIState>((set, get) => ({
  viewMode: 'table',
  selectedTicketIds: [],
  isFilterDrawerOpen: false,

  setViewMode: (mode) => set({ viewMode: mode }),

  toggleTicketSelection: (ticketId) => 
    set((state) => {
      const isSelected = state.selectedTicketIds.includes(ticketId);
      return {
        selectedTicketIds: isSelected
          ? state.selectedTicketIds.filter(id => id !== ticketId)
          : [...state.selectedTicketIds, ticketId]
      };
    }),

  selectAll: (ticketIds) =>
    set({ selectedTicketIds: ticketIds }),

  clearSelection: () =>
    set({ selectedTicketIds: [] }),

  toggleFilterDrawer: () =>
    set((state) => ({ isFilterDrawerOpen: !state.isFilterDrawerOpen })),

  // Computed properties
  get hasSelection() {
    return get().selectedTicketIds.length > 0;
  },

  get selectedCount() {
    return get().selectedTicketIds.length;
  },
}));

Why Zustand over Context:

  • No provider needed: Use anywhere in your app
  • Selective subscriptions: Components only re-render when their slice changes
  • Middleware built-in: Persist, devtools, etc.
  • Simple API: No reducers, no actions, no boilerplate

Part 6: Building the UI Components

Now let's build the actual UI, using our hooks and stores.

Step 1: Create Shared UI Components

First, let's build reusable components that will be used throughout the app:

Code
// src/shared/ui/Button.tsx

import { Slot } from '@radix-ui/react-slot';
import { forwardRef } from 'react';

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  asChild?: boolean;
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ 
    asChild = false, 
    variant = 'primary', 
    size = 'md', 
    isLoading = false,
    className = '',
    children, 
    disabled,
    ...props 
  }, ref) => {
    const Comp = asChild ? Slot : 'button';
    
    const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
    
    const variants = {
      primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
      secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
      outline: 'border border-gray-300 bg-transparent hover:bg-gray-50 focus:ring-blue-500',
      ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500',
      danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
    };
    
    const sizes = {
      sm: 'px-3 py-1.5 text-sm gap-1.5',
      md: 'px-4 py-2 text-base gap-2',
      lg: 'px-6 py-3 text-lg gap-2.5',
    };
    
    return (
      <Comp
        ref={ref}
        className={`${baseClasses} ${variants[variant]} ${sizes[size]} ${className}`}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading && (
          <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
          </svg>
        )}
        {children}
      </Comp>
    );
  }
);

Button.displayName = 'Button';
Code
// src/shared/ui/Select.tsx

import { forwardRef, SelectHTMLAttributes } from 'react';

interface SelectOption {
  value: string;
  label: string;
}

interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
  options: SelectOption[];
  error?: string;
  label?: string;
}

export const Select = forwardRef<HTMLSelectElement, SelectProps>(
  ({ options, error, label, className = '', ...props }, ref) => {
    return (
      <div className="w-full">
        {label && (
          <label className="block text-sm font-medium text-gray-700 mb-1">
            {label}
          </label>
        )}
        <select
          ref={ref}
          className={`
            block w-full rounded-md border-gray-300 shadow-sm
            focus:border-blue-500 focus:ring-blue-500 sm:text-sm
            ${error ? 'border-red-500' : ''}
            ${className}
          `}
          {...props}
        >
          {options.map(option => (
            <option key={option.value} value={option.value}>
              {option.label}
            </option>
          ))}
        </select>
        {error && (
          <p className="mt-1 text-sm text-red-600">{error}</p>
        )}
      </div>
    );
  }
);

Select.displayName = 'Select';

Step 2: Build Feature Components

Now the ticket-specific components:

Code
// src/features/tickets/components/TicketFilters.tsx

'use client';

import { useFilterStore } from '../store/filterStore';
import { TICKET_STATUSES, TICKET_PRIORITIES } from '@/entities/ticket/constants';
import { Button } from '@/shared/ui/Button';
import { Select } from '@/shared/ui/Select';
import { Input } from '@/shared/ui/Input';

export function TicketFilters() {
  const { filters, setStatus, setPriority, setSearch, resetFilters } = useFilterStore();
  const activeFilterCount = useFilterStore((state) => state.getActiveFilterCount());

  return (
    <div className="bg-white p-4 rounded-lg shadow space-y-4">
      <div className="flex justify-between items-center">
        <h3 className="font-medium">Filters</h3>
        {activeFilterCount > 0 && (
          <Button variant="ghost" size="sm" onClick={resetFilters}>
            Clear all
          </Button>
        )}
      </div>

      <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
        <Select
          label="Status"
          options={[
            { value: '', label: 'All Statuses' },
            ...TICKET_STATUSES.map(status => ({
              value: status,
              label: status.charAt(0).toUpperCase() + status.slice(1)
            }))
          ]}
          value={filters.status?.[0] || ''}
          onChange={(e) => setStatus(e.target.value ? [e.target.value as any] : [])}
        />

        <Select
          label="Priority"
          options={[
            { value: '', label: 'All Priorities' },
            ...TICKET_PRIORITIES.map(priority => ({
              value: priority,
              label: priority.charAt(0).toUpperCase() + priority.slice(1)
            }))
          ]}
          value={filters.priority?.[0] || ''}
          onChange={(e) => setPriority(e.target.value ? [e.target.value as any] : [])}
        />

        <Input
          label="Search"
          type="search"
          placeholder="Search tickets..."
          value={filters.search || ''}
          onChange={(e) => setSearch(e.target.value)}
        />

        <div className="flex items-end">
          <Button variant="secondary" onClick={() => console.log('Apply filters')}>
            Apply Filters
          </Button>
        </div>
      </div>
    </div>
  );
}
Code
// src/features/tickets/components/TicketTable.tsx

'use client';

import { useTickets } from '../hooks/useTickets';
import { useFilterStore } from '../store/filterStore';
import { useUIStore } from '../store/uiStore';
import { TICKET_STATUS_COLORS, TICKET_PRIORITY_COLORS } from '@/entities/ticket/constants';
import { formatDistanceToNow } from 'date-fns';
import { Button } from '@/shared/ui/Button';
import { Checkbox } from '@/shared/ui/Checkbox';
import { useRouter } from 'next/navigation';

export function TicketTable() {
  const router = useRouter();
  const filters = useFilterStore((state) => state.filters);
  const { data: tickets, isLoading, isError } = useTickets(filters);
  
  const { selectedTicketIds, toggleTicketSelection, selectAll, clearSelection } = useUIStore();

  if (isLoading) {
    return <TicketTableSkeleton />;
  }

  if (isError) {
    return (
      <div className="text-center py-12">
        <p className="text-red-600">Failed to load tickets</p>
        <Button onClick={() => window.location.reload()} className="mt-4">
          Retry
        </Button>
      </div>
    );
  }

  if (!tickets?.length) {
    return (
      <div className="text-center py-12 bg-gray-50 rounded-lg">
        <p className="text-gray-600">No tickets found</p>
      </div>
    );
  }

  const handleSelectAll = () => {
    if (selectedTicketIds.length === tickets.length) {
      clearSelection();
    } else {
      selectAll(tickets.map(t => t.id));
    }
  };

  return (
    <div className="bg-white rounded-lg shadow overflow-hidden">
      <div className="overflow-x-auto">
        <table className="min-w-full divide-y divide-gray-200">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-6 py-3 text-left">
                <Checkbox
                  checked={selectedTicketIds.length === tickets.length}
                  indeterminate={selectedTicketIds.length > 0 && selectedTicketIds.length < tickets.length}
                  onChange={handleSelectAll}
                />
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                ID
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Title
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Status
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Priority
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Assigned To
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Created
              </th>
              <th className="px-6 py-3 text-right">Actions</th>
            </tr>
          </thead>
          <tbody className="bg-white divide-y divide-gray-200">
            {tickets.map((ticket) => (
              <tr 
                key={ticket.id} 
                className="hover:bg-gray-50 cursor-pointer"
                onClick={() => router.push(`/tickets/${ticket.id}`)}
              >
                <td className="px-6 py-4" onClick={(e) => e.stopPropagation()}>
                  <Checkbox
                    checked={selectedTicketIds.includes(ticket.id)}
                    onChange={() => toggleTicketSelection(ticket.id)}
                  />
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm font-mono">
                  {ticket.id.slice(0, 8)}
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
                  {ticket.title}
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${TICKET_STATUS_COLORS[ticket.status]}`}>
                    {ticket.status}
                  </span>
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${TICKET_PRIORITY_COLORS[ticket.priority]}`}>
                    {ticket.priority}
                  </span>
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm">
                  {ticket.assignedToId ? 'Agent Name' : 'Unassigned'}
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                  {formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                  <Button 
                    variant="ghost" 
                    size="sm"
                    onClick={(e) => {
                      e.stopPropagation();
                      router.push(`/tickets/${ticket.id}/edit`);
                    }}
                  >
                    Edit
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function TicketTableSkeleton() {
  return (
    <div className="bg-white rounded-lg shadow overflow-hidden">
      <div className="animate-pulse">
        <div className="bg-gray-50 px-6 py-3">
          <div className="h-4 bg-gray-200 rounded w-1/4"></div>
        </div>
        {[...Array(5)].map((_, i) => (
          <div key={i} className="px-6 py-4 border-t">
            <div className="h-4 bg-gray-200 rounded w-3/4"></div>
          </div>
        ))}
      </div>
    </div>
  );
}

Step 3: Compose the Page

Finally, let's put it all together in the page:

Code
// src/app/tickets/page.tsx

import { Suspense } from 'react';
import { TicketFilters } from '@/features/tickets/components/TicketFilters';
import { TicketTable } from '@/features/tickets/components/TicketTable';
import { Button } from '@/shared/ui/Button';
import Link from 'next/link';

export default function TicketsPage() {
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="flex justify-between items-center mb-8">
        <h1 className="text-3xl font-bold">Support Tickets</h1>
        <Link href="/tickets/new">
          <Button>Create Ticket</Button>
        </Link>
      </div>

      <div className="space-y-6">
        <TicketFilters />
        
        <Suspense fallback={<div>Loading tickets...</div>}>
          <TicketTable />
        </Suspense>
      </div>
    </div>
  );
}

Part 7: Advanced Patterns

Now that we have a working feature, let's add some advanced patterns that make the code more maintainable and performant.

Pattern 1: The Compound Component Pattern

For complex components like the filter bar, compound components provide a flexible API:

Code
// src/features/tickets/components/FilterBar/FilterBar.tsx

import { createContext, useContext, useState } from 'react';

interface FilterBarContextValue {
  activeFilters: Set<string>;
  toggleFilter: (name: string) => void;
}

const FilterBarContext = createContext<FilterBarContextValue | null>(null);

function useFilterBar() {
  const context = useContext(FilterBarContext);
  if (!context) {
    throw new Error('FilterBar components must be used within FilterBar');
  }
  return context;
}

interface FilterBarProps {
  children: React.ReactNode;
  onApply?: (filters: string[]) => void;
}

export function FilterBar({ children, onApply }: FilterBarProps) {
  const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set());

  const toggleFilter = (name: string) => {
    setActiveFilters(prev => {
      const next = new Set(prev);
      if (next.has(name)) {
        next.delete(name);
      } else {
        next.add(name);
      }
      return next;
    });
  };

  const handleApply = () => {
    onApply?.(Array.from(activeFilters));
  };

  return (
    <FilterBarContext.Provider value={{ activeFilters, toggleFilter }}>
      <div className="bg-white p-4 rounded-lg shadow">
        <div className="flex flex-wrap gap-4">
          {children}
        </div>
        <div className="mt-4 flex justify-end">
          <button
            onClick={handleApply}
            className="px-4 py-2 bg-blue-600 text-white rounded-md"
          >
            Apply Filters
          </button>
        </div>
      </div>
    </FilterBarContext.Provider>
  );
}

interface FilterOptionProps {
  name: string;
  label: string;
}

FilterBar.Option = function FilterOption({ name, label }: FilterOptionProps) {
  const { activeFilters, toggleFilter } = useFilterBar();
  const isActive = activeFilters.has(name);

  return (
    <button
      onClick={() => toggleFilter(name)}
      className={`
        px-3 py-1 rounded-full text-sm font-medium
        ${isActive 
          ? 'bg-blue-600 text-white' 
          : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
        }
      `}
    >
      {label}
    </button>
  );
};

// Usage:
<FilterBar onApply={(filters) => console.log(filters)}>
  <FilterBar.Option name="open" label="Open" />
  <FilterBar.Option name="in-progress" label="In Progress" />
  <FilterBar.Option name="resolved" label="Resolved" />
  <FilterBar.Option name="high-priority" label="High Priority" />
</FilterBar>

Pattern 2: The Render Props Pattern for Data Loading

Sometimes you need to share data loading logic without committing to a UI:

Code
// src/features/tickets/components/TicketDataLoader.tsx

import { useTickets } from '../hooks/useTickets';
import { TicketFilters } from '@/entities/ticket/types';

interface TicketDataLoaderProps {
  filters?: TicketFilters;
  children: (props: {
    tickets: Ticket[];
    isLoading: boolean;
    isError: boolean;
    refetch: () => void;
  }) => React.ReactNode;
}

export function TicketDataLoader({ filters, children }: TicketDataLoaderProps) {
  const { data: tickets = [], isLoading, isError, refetch } = useTickets(filters);
  
  return children({ tickets, isLoading, isError, refetch });
}

// Usage:
<TicketDataLoader filters={{ status: ['open'] }}>
  {({ tickets, isLoading }) => (
    <div>
      {isLoading ? (
        <Spinner />
      ) : (
        tickets.map(ticket => <TicketCard key={ticket.id} ticket={ticket} />)
      )}
    </div>
  )}
</TicketDataLoader>

Pattern 3: The Factory Pattern for Hooks

When you have similar hooks across different entities, a factory pattern reduces duplication:

Code
// src/shared/lib/createEntityHooks.ts

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ticketsApi } from '@/features/tickets/api/client';

interface EntityConfig<T, F> {
  baseKey: string;
  getById: (id: string) => Promise<T>;
  getList: (filters?: F) => Promise<T[]>;
  create: (data: Partial<T>) => Promise<T>;
  update: (id: string, data: Partial<T>) => Promise<T>;
  delete: (id: string) => Promise<void>;
}

export function createEntityHooks<T, F = any>(config: EntityConfig<T, F>) {
  const keys = {
    all: [config.baseKey] as const,
    lists: () => [...keys.all, 'list'] as const,
    list: (filters?: F) => [...keys.lists(), filters] as const,
    details: () => [...keys.all, 'detail'] as const,
    detail: (id: string) => [...keys.details(), id] as const,
  };

  function useList(filters?: F) {
    return useQuery({
      queryKey: keys.list(filters),
      queryFn: () => config.getList(filters),
    });
  }

  function useOne(id: string) {
    const queryClient = useQueryClient();

    const query = useQuery({
      queryKey: keys.detail(id),
      queryFn: () => config.getById(id),
    });

    const updateMutation = useMutation({
      mutationFn: (data: Partial<T>) => config.update(id, data),
      onMutate: async (newData) => {
        await queryClient.cancelQueries({ queryKey: keys.detail(id) });
        const previous = queryClient.getQueryData<T>(keys.detail(id));
        queryClient.setQueryData<T>(keys.detail(id), (old) => ({
          ...old!,
          ...newData,
        }));
        return { previous };
      },
      onError: (err, newData, context) => {
        queryClient.setQueryData(keys.detail(id), context?.previous);
      },
    });

    return {
      data: query.data,
      isLoading: query.isLoading,
      update: updateMutation.mutate,
      isUpdating: updateMutation.isPending,
    };
  }

  return { useList, useOne };
}

// Usage:
export const ticketHooks = createEntityHooks({
  baseKey: 'tickets',
  getById: ticketsApi.getTicketById,
  getList: ticketsApi.getTickets,
  create: ticketsApi.createTicket,
  update: ticketsApi.updateTicket,
  delete: ticketsApi.deleteTicket,
});

Part 8: Testing Strategy

A robust architecture needs robust tests. Let's write tests for our key pieces.

Step 1: Unit Test a Hook

Code
// src/features/tickets/hooks/__tests__/useTickets.test.ts

import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useTickets } from '../useTickets';
import { ticketsApi } from '../../api/client';

// Mock the API
jest.mock('../../api/client');

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
  },
});

const wrapper = ({ children }: { children: React.ReactNode }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe('useTickets', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('fetches tickets successfully', async () => {
    const mockTickets = [
      { id: '1', title: 'Ticket 1' },
      { id: '2', title: 'Ticket 2' },
    ];

    (ticketsApi.getTickets as jest.Mock).mockResolvedValue(mockTickets);

    const { result } = renderHook(() => useTickets(), { wrapper });

    expect(result.current.isLoading).toBe(true);

    await waitFor(() => expect(result.current.isSuccess).toBe(true));

    expect(result.current.data).toEqual(mockTickets);
    expect(ticketsApi.getTickets).toHaveBeenCalledTimes(1);
  });

  it('handles error states', async () => {
    const error = new Error('Failed to fetch');
    (ticketsApi.getTickets as jest.Mock).mockRejectedValue(error);

    const { result } = renderHook(() => useTickets(), { wrapper });

    await waitFor(() => expect(result.current.isError).toBe(true));

    expect(result.current.error).toBeDefined();
  });

  it('applies filters correctly', async () => {
    const filters = { status: ['open'], priority: ['high'] };
    
    renderHook(() => useTickets(filters), { wrapper });

    expect(ticketsApi.getTickets).toHaveBeenCalledWith(filters);
  });
});

Step 2: Component Test

Code
// src/features/tickets/components/__tests__/TicketFilters.test.tsx

import { render, screen, fireEvent } from '@testing-library/react';
import { TicketFilters } from '../TicketFilters';
import { useFilterStore } from '../../store/filterStore';

// Mock the store
jest.mock('../../store/filterStore');

describe('TicketFilters', () => {
  const mockSetStatus = jest.fn();
  const mockSetPriority = jest.fn();
  const mockSetSearch = jest.fn();

  beforeEach(() => {
    (useFilterStore as jest.Mock).mockImplementation((selector) => {
      const state = {
        filters: {
          status: [],
          priority: [],
          search: '',
        },
        setStatus: mockSetStatus,
        setPriority: mockSetPriority,
        setSearch: mockSetSearch,
        getActiveFilterCount: () => 0,
      };
      return selector ? selector(state) : state;
    });
  });

  it('renders all filter inputs', () => {
    render(<TicketFilters />);

    expect(screen.getByLabelText(/status/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/priority/i)).toBeInTheDocument();
    expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
  });

  it('updates search input', () => {
    render(<TicketFilters />);

    const searchInput = screen.getByPlaceholderText(/search/i);
    fireEvent.change(searchInput, { target: { value: 'bug' } });

    expect(mockSetSearch).toHaveBeenCalledWith('bug');
  });

  it('clears filters when clear button is clicked', () => {
    (useFilterStore as jest.Mock).mockImplementation((selector) => {
      const state = {
        filters: {
          status: ['open'],
          priority: ['high'],
          search: 'test',
        },
        setStatus: mockSetStatus,
        setPriority: mockSetPriority,
        setSearch: mockSetSearch,
        resetFilters: jest.fn(),
        getActiveFilterCount: () => 3,
      };
      return selector ? selector(state) : state;
    });

    render(<TicketFilters />);

    const clearButton = screen.getByText(/clear all/i);
    fireEvent.click(clearButton);

    expect(mockSetStatus).toHaveBeenCalledWith([]);
    expect(mockSetPriority).toHaveBeenCalledWith([]);
    expect(mockSetSearch).toHaveBeenCalledWith('');
  });
});

Step 3: Integration Test

Code
// src/features/tickets/__tests__/TicketFlow.test.tsx

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TicketsPage } from '@/app/tickets/page';
import { ticketsApi } from '../api/client';

// Mock the API
jest.mock('../api/client');

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
  },
});

describe('Ticket Management Flow', () => {
  beforeEach(() => {
    queryClient.clear();
    jest.clearAllMocks();
  });

  it('loads and displays tickets', async () => {
    const mockTickets = [
      {
        id: '1',
        title: 'First Ticket',
        status: 'open',
        priority: 'high',
        createdAt: new Date().toISOString(),
      },
      {
        id: '2',
        title: 'Second Ticket',
        status: 'in-progress',
        priority: 'medium',
        createdAt: new Date().toISOString(),
      },
    ];

    (ticketsApi.getTickets as jest.Mock).mockResolvedValue(mockTickets);

    render(
      <QueryClientProvider client={queryClient}>
        <TicketsPage />
      </QueryClientProvider>
    );

    // Should show loading state initially
    expect(screen.getByText(/loading tickets/i)).toBeInTheDocument();

    // Wait for tickets to load
    await waitFor(() => {
      expect(screen.getByText('First Ticket')).toBeInTheDocument();
    });

    expect(screen.getByText('Second Ticket')).toBeInTheDocument();
  });

  it('filters tickets when filter is applied', async () => {
    const mockTickets = [
      {
        id: '1',
        title: 'Open Ticket',
        status: 'open',
        priority: 'high',
        createdAt: new Date().toISOString(),
      },
      {
        id: '2',
        title: 'Closed Ticket',
        status: 'closed',
        priority: 'low',
        createdAt: new Date().toISOString(),
      },
    ];

    (ticketsApi.getTickets as jest.Mock).mockResolvedValue(mockTickets);

    render(
      <QueryClientProvider client={queryClient}>
        <TicketsPage />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText('Open Ticket')).toBeInTheDocument();
    });

    // Apply status filter
    const statusSelect = screen.getByLabelText(/status/i);
    await userEvent.selectOptions(statusSelect, 'open');

    // API should be called with filters
    expect(ticketsApi.getTickets).toHaveBeenCalledWith(
      expect.objectContaining({ status: ['open'] })
    );
  });
});

Part 9: Performance Optimization

Let's optimize our application for production.

Step 1: Code Splitting with Next.js

Next.js automatically code-splits by routes, but we can also lazy-load heavy components:

Code
// src/features/tickets/components/TicketAnalytics.tsx

import dynamic from 'next/dynamic';
import { Suspense } from 'react';

// Heavy chart library - only loaded when needed
const TicketChart = dynamic(
  () => import('./TicketChart').then(mod => mod.TicketChart),
  {
    loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded" />,
    ssr: false, // Don't render on server
  }
);

export function TicketAnalytics({ ticketId }: { ticketId: string }) {
  const [showCharts, setShowCharts] = useState(false);

  return (
    <div>
      <button onClick={() => setShowCharts(!showCharts)}>
        {showCharts ? 'Hide' : 'Show'} Analytics
      </button>

      {showCharts && (
        <Suspense fallback={<div>Loading charts...</div>}>
          <TicketChart ticketId={ticketId} />
        </Suspense>
      )}
    </div>
  );
}

Step 2: Virtualization for Large Lists

When rendering many tickets, virtualization prevents DOM overload:

Code
// src/features/tickets/components/VirtualizedTicketList.tsx

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
import { Ticket } from '@/entities/ticket/types';

interface VirtualizedTicketListProps {
  tickets: Ticket[];
  onTicketClick: (id: string) => void;
}

export function VirtualizedTicketList({ tickets, onTicketClick }: VirtualizedTicketListProps) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: tickets.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80, // Height of each row
    overscan: 5, // Render 5 extra rows above/below viewport
  });

  return (
    <div
      ref={parentRef}
      className="h-[600px] overflow-auto border rounded"
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <TicketRow
              ticket={tickets[virtualRow.index]}
              onClick={() => onTicketClick(tickets[virtualRow.index].id)}
            />
          </div>
        ))}
      </div>
    </div>
  );
}

function TicketRow({ ticket, onClick }: { ticket: Ticket; onClick: () => void }) {
  return (
    <div
      onClick={onClick}
      className="p-4 border-b hover:bg-gray-50 cursor-pointer"
    >
      <div className="flex justify-between">
        <h3 className="font-medium">{ticket.title}</h3>
        <span className="text-sm text-gray-500">
          {ticket.status}
        </span>
      </div>
    </div>
  );
}

Step 3: Memoization Strategy

With the React Compiler, we need less manual memoization, but sometimes we still need control:

Code
// src/features/tickets/components/TicketCard.tsx

import { memo } from 'react';
import { Ticket } from '@/entities/ticket/types';
import { TICKET_STATUS_COLORS } from '@/entities/ticket/constants';

interface TicketCardProps {
  ticket: Ticket;
  onSelect: (id: string) => void;
  isSelected: boolean;
}

// Memoize the entire component
export const TicketCard = memo(function TicketCard({ 
  ticket, 
  onSelect, 
  isSelected 
}: TicketCardProps) {
  return (
    <div
      className={`p-4 border rounded-lg cursor-pointer transition-colors ${
        isSelected ? 'bg-blue-50 border-blue-300' : 'hover:bg-gray-50'
      }`}
      onClick={() => onSelect(ticket.id)}
    >
      <div className="flex justify-between items-start">
        <h3 className="font-medium">{ticket.title}</h3>
        <span className={`px-2 py-1 text-xs rounded-full ${TICKET_STATUS_COLORS[ticket.status]}`}>
          {ticket.status}
        </span>
      </div>
      
      <p className="text-sm text-gray-600 mt-2 line-clamp-2">
        {ticket.description}
      </p>
      
      <div className="flex justify-between items-center mt-4 text-sm text-gray-500">
        <span>Priority: {ticket.priority}</span>
        <span>
          {new Date(ticket.createdAt).toLocaleDateString()}
        </span>
      </div>
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison to prevent unnecessary re-renders
  return (
    prevProps.ticket.id === nextProps.ticket.id &&
    prevProps.ticket.status === nextProps.ticket.status &&
    prevProps.ticket.priority === nextProps.ticket.priority &&
    prevProps.isSelected === nextProps.isSelected
  );
});

Step 4: Bundle Analysis

Add bundle analysis to your build process:

Code
npm install -D @next/bundle-analyzer
Code
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

/** @type {import('next').NextConfig} */
const nextConfig = {
  // your config
};

module.exports = withBundleAnalyzer(nextConfig);
Code
// package.json
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build"
  }
}

Part 10: Deployment and Monitoring

Step 1: Environment Configuration

Code
// src/shared/lib/env.ts

import { z } from 'zod';

const envSchema = z.object({
  // Required
  NODE_ENV: z.enum(['development', 'production', 'test']),
  NEXT_PUBLIC_API_URL: z.string().url(),
  
  // Optional with defaults
  NEXT_PUBLIC_WS_URL: z.string().url().optional().default('ws://localhost:3001'),
  NEXT_PUBLIC_ANALYTICS_ID: z.string().optional(),
  
  // Server-side only
  DATABASE_URL: z.string().url(),
  API_SECRET: z.string().min(32),
});

// Parse and validate
const env = envSchema.parse({
  NODE_ENV: process.env.NODE_ENV,
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL,
  NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
  DATABASE_URL: process.env.DATABASE_URL,
  API_SECRET: process.env.API_SECRET,
});

export const config = {
  api: {
    baseUrl: env.NEXT_PUBLIC_API_URL,
    wsUrl: env.NEXT_PUBLIC_WS_URL,
  },
  analytics: {
    id: env.NEXT_PUBLIC_ANALYTICS_ID,
  },
  database: {
    url: env.DATABASE_URL,
  },
  isDev: env.NODE_ENV === 'development',
  isProd: env.NODE_ENV === 'production',
} as const;

Step 2: Error Tracking with Error Boundaries

Code
// src/features/error/ErrorBoundary.tsx

'use client';

import { Component, ErrorInfo, ReactNode } from 'react';
import { Button } from '@/shared/ui/Button';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => 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) {
    // Log to error tracking service
    console.error('Error caught by boundary:', error, errorInfo);
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }

      return (
        <div className="min-h-[400px] flex items-center justify-center">
          <div className="text-center">
            <h2 className="text-2xl font-bold text-red-600 mb-4">
              Something went wrong
            </h2>
            <p className="text-gray-600 mb-6">
              {this.state.error?.message || 'An unexpected error occurred'}
            </p>
            <Button
              onClick={() => {
                this.setState({ hasError: false });
                window.location.reload();
              }}
            >
              Try again
            </Button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

Usage:

Code
// src/app/tickets/page.tsx
import { ErrorBoundary } from '@/features/error/ErrorBoundary';

export default function TicketsPage() {
  return (
    <ErrorBoundary>
      <TicketTable />
    </ErrorBoundary>
  );
}

Step 3: Analytics Tracking

Code
// src/features/analytics/track.ts

type EventName = 'ticket_viewed' | 'ticket_updated' | 'filter_applied' | 'search_performed';

interface EventProperties {
  ticket_id?: string;
  filter_type?: string;
  search_term?: string;
  [key: string]: any;
}

class Analytics {
  private isEnabled = config.analytics.id && !config.isDev;

  track(eventName: EventName, properties?: EventProperties) {
    if (!this.isEnabled) return;

    // Send to analytics service
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify({
        event: eventName,
        properties,
        timestamp: new Date().toISOString(),
      }),
    }).catch(console.error);
  }

  pageView(path: string) {
    this.track('page_view' as any, { path });
  }
}

export const analytics = new Analytics();

// Usage in components:
analytics.track('ticket_viewed', { ticket_id: '123' });

Part 11: The Complete Architecture Review

Let's step back and look at what we've built:

Layer 1: Domain Entities

  • Pure TypeScript types
  • No framework dependencies
  • Single source of truth

Layer 2: API Layer

  • REST client with typed methods
  • WebSocket for real-time updates
  • Singleton pattern for shared instances

Layer 3: Data Access Hooks

  • React Query for server state
  • Optimistic updates
  • Automatic cache management

Layer 4: Client State

  • Zustand stores for UI state
  • Persisted filters
  • Computed selectors

Layer 5: UI Components

  • Shared design system
  • Feature-specific components
  • Compound components for complex UIs

Layer 6: Routing

  • Next.js App Router
  • Route-level code splitting
  • Server Components by default

Layer 7: Testing

  • Unit tests for hooks
  • Component tests with RTL
  • Integration tests for flows

Layer 8: Performance

  • Code splitting
  • Virtualization
  • Memoization strategy
  • Bundle analysis

Layer 9: Monitoring

  • Error boundaries
  • Analytics tracking
  • Environment configuration

The Architecture Decision Tree

Here's how I make architectural decisions now:

Question 1: Where does this data live?

  • Server (database) β†’ React Query
  • Client (UI state) β†’ Zustand
  • URL (shareable) β†’ Search params

Question 2: Where does this component render?

  • Static content β†’ Server Component
  • Interactive but no state β†’ Server Component + Client event handlers
  • Has state or effects β†’ Client Component

Question 3: How reusable is this?

  • Only used in one feature β†’ Keep in feature folder
  • Used in multiple features β†’ Move to shared/ui
  • Core business concept β†’ Move to entities

Question 4: How complex is this interaction?

  • Simple form β†’ Server Action
  • Complex state β†’ Zustand store
  • Real-time β†’ WebSocket + React Query

What I'd Tell My Past Self

Building this architecture taught me lessons I wish I'd known years ago:

Start with types, not components. Define your domain models first. Everything else flows from them.

State management is about boundaries, not tools. The question isn't "Redux or Zustand?"β€”it's "which state belongs where?"

Test behavior, not implementation. Test that your component shows tickets when data loads, not that it called a specific function.

Performance is a feature, not an afterthought. Build with virtualization, code splitting, and memoization in mind from day one.

Your architecture will evolve. What works for a team of 5 won't work for 50. Design for change.

The best pattern is the one you understand. Don't adopt a pattern just because it's trendy. Adopt it because it solves a real problem you have.


Next Steps

You've built a production-ready architecture. Here's what to do with it:

  1. Extend the entities β€” Add more fields, more relationships
  2. Add more features β€” User management, reporting, exports
  3. Implement the backend β€” Build the actual API
  4. Add authentication β€” Protect routes, add permissions
  5. Deploy to production β€” Vercel makes this trivial

The code we've written together is a starting point, not a finish line. Every application has unique needs. Take these patterns, adapt them, make them your own.

And when something breaksβ€”because it willβ€”you'll have the architecture to fix it without tearing everything down.


Resources


Enjoyed this tutorial? I write about React architecture and patterns every Month. Follow me on Twitter @themarvelbiz


Written by Merveille Alexander

Software Engineer sharing knowledge and experience.