# React Frontend Architecture Guide ## Building Enterprise Applications with Three-Layer Service Architecture **Version:** 2.0.0 **React:** 19.1.0 **Build Tool:** Vite 7.1.2 **Styling:** Tailwind CSS 4.1.12 --- ## Table of Contents 1. [Project Overview](#project-overview) 2. [Technology Stack](#technology-stack) 3. [Project Structure](#project-structure) 4. [Three-Layer Service Architecture](#three-layer-service-architecture) 5. [Dependency Injection System](#dependency-injection-system) 6. [Constants Management](#constants-management) 7. [Component Architecture](#component-architecture) 8. [Authentication & Authorization](#authentication--authorization) 9. [Routing & Navigation](#routing--navigation) 10. [State Management & Caching](#state-management--caching) 11. [Styling & Theming](#styling--theming) 12. [Build Configuration](#build-configuration) 13. [Complete Setup Guide](#complete-setup-guide) 14. [Best Practices & Patterns](#best-practices--patterns) --- ## Project Overview This architecture guide documents a production-grade React application built for enterprise use. The application follows a strict **three-layer service architecture** (API, Storage, Manager) with custom dependency injection, comprehensive constants management, and zero tolerance for magic values. ### Core Principles 1. **No Magic Values**: Every number, string, or configuration value must be defined as a constant 2. **Service Layer Separation**: Clear boundaries between API, Storage, and Manager layers 3. **Dependency Injection**: Custom DI system without third-party libraries 4. **Component Reusability**: Extensive component library organized by purpose 5. **Type Safety**: PropTypes for runtime type checking 6. **Performance**: Code splitting, caching, and optimization strategies --- ## Technology Stack ### Core Dependencies ```json { "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.8.0", "vite": "^7.1.2", "tailwindcss": "^4.1.12" } ``` ### Key Libraries - **HTTP Client**: `axios` (^1.11.0) - API communication with interceptors - **Date Handling**: `luxon` (^3.7.1) - Date manipulation and formatting - **Case Conversion**: `humps` (^2.0.1) - camelCase ↔ snake_case conversion - **Icons**: `@heroicons/react` (^2.2.0) - SVG icon library - **QR Codes**: `qrcode.react` (^4.2.0) - 2FA QR code generation - **Utility**: `clsx` (^2.1.1) - Conditional className utility ### Development Tools - **Linting**: ESLint 9 with React plugins - **Build Optimization**: Terser for minification - **CSS Processing**: PostCSS with Autoprefixer --- ## Project Structure ``` web/frontend/ ├── index.html # Entry HTML file ├── package.json # Dependencies and scripts ├── vite.config.js # Vite build configuration ├── tailwind.config.js # Tailwind CSS configuration ├── eslint.config.js # ESLint configuration │ ├── public/ # Static assets │ ├── favicon.ico │ └── assets/ │ └── src/ ├── main.jsx # Application entry point ├── AppRouter.jsx # Main routing configuration │ ├── components/ # Reusable components │ ├── UI/ # Pure UI components │ │ ├── Button/ │ │ ├── Input/ │ │ ├── Modal/ │ │ ├── Table/ │ │ └── ... │ ├── UIX/ # Enhanced UI components │ │ ├── EntityListPage/ │ │ ├── EntityDetailPage/ │ │ └── ... │ ├── business/ # Domain-aware components │ │ ├── CustomerSelect/ │ │ ├── OrderDisplay/ │ │ └── ... │ └── Layout/ # Layout components │ ├── Sidebar.jsx │ ├── Header.jsx │ └── Footer.jsx │ ├── pages/ # Page components (route handlers) │ ├── Admin/ │ │ ├── Customer/ │ │ │ ├── List/ │ │ │ │ └── Page.jsx │ │ │ ├── Detail/ │ │ │ │ └── Page.jsx │ │ │ ├── Create/ │ │ │ │ └── Page.jsx │ │ │ └── Update/ │ │ │ └── Page.jsx │ │ └── ... │ ├── Dashboard/ │ └── Auth/ │ ├── services/ # Service layer (CRITICAL) │ ├── Services.jsx # DI container │ ├── Config/ │ │ └── APIConfig.js │ ├── API/ # HTTP communication layer │ │ ├── CustomerAPI.js │ │ ├── AuthAPI.js │ │ └── ... │ ├── Storage/ # Local persistence layer │ │ ├── CustomerStorage.js │ │ ├── TokenStorage.js │ │ └── ... │ ├── Manager/ # Business logic layer │ │ ├── CustomerManager.js │ │ ├── AuthManager.js │ │ └── ... │ └── Helpers/ │ └── AuthenticatedAxios.js │ ├── constants/ # Constants (NO MAGIC VALUES!) │ ├── Customer.js │ ├── Authentication.js │ ├── UI.js │ ├── Roles.js │ └── ... │ ├── utils/ # Utility functions │ └── serviceUtils.js │ ├── styles/ # Global styles │ └── app.css │ └── assets/ # Static assets (images, fonts) ``` --- ## Three-Layer Service Architecture The most critical aspect of this architecture is the **three-layer service pattern**. This pattern ensures: - Clear separation of concerns - Testability - Maintainability - Consistent error handling - Caching strategies ### Layer 1: API Layer (`src/services/API/`) **Purpose**: HTTP communication with backend services **Responsibilities**: - Making HTTP requests using Axios - Request/response transformation (camelCase ↔ snake_case) - Error handling and formatting - Endpoint configuration **Example: CustomerAPI.js** ```javascript // src/services/API/CustomerAPI.js import { camelizeKeys, decamelizeKeys, decamelize } from "humps"; import { createAuthenticatedAxios } from "../Helpers/AuthenticatedAxios"; import { DateTime } from "luxon"; export class CustomerAPI { constructor(baseURL, endpoints, tokenStorage) { this.baseURL = baseURL; this.endpoints = endpoints; this.tokenStorage = tokenStorage; } /** * Gets list of customers with filtering and pagination */ async getCustomers(params = {}, onUnauthorizedCallback = null) { try { // Create authenticated axios instance const authenticatedAxios = createAuthenticatedAxios( this.baseURL, this.tokenStorage, onUnauthorizedCallback ); // Build query parameters const queryParams = new URLSearchParams(); if (params.page) queryParams.append("page", params.page); if (params.limit) queryParams.append("page_size", params.limit); if (params.search) queryParams.append("search", params.search); // Make API call const response = await authenticatedAxios.get( `${this.endpoints.CUSTOMERS}?${queryParams.toString()}` ); // Convert snake_case to camelCase const data = camelizeKeys(response.data); // Format dates if (data.results) { data.results.forEach((item) => { if (item.createdAt) { item.createdAt = DateTime.fromISO(item.createdAt) .toLocaleString(DateTime.DATETIME_MED); } }); } return data; } catch (error) { throw this._formatError(error); } } /** * Creates a new customer */ async createCustomer(customerData, onUnauthorizedCallback = null) { try { const authenticatedAxios = createAuthenticatedAxios( this.baseURL, this.tokenStorage, onUnauthorizedCallback ); // Convert camelCase to snake_case for backend let decamelizedData = decamelizeKeys(customerData); const response = await authenticatedAxios.post( this.endpoints.CUSTOMERS, decamelizedData ); return camelizeKeys(response.data); } catch (error) { throw this._formatError(error); } } /** * Gets customer detail by ID */ async getCustomerDetail(customerId, onUnauthorizedCallback = null) { try { if (!customerId) { throw { customerId: "Valid customer ID is required" }; } const authenticatedAxios = createAuthenticatedAxios( this.baseURL, this.tokenStorage, onUnauthorizedCallback ); const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId); const response = await authenticatedAxios.get(url); return camelizeKeys(response.data); } catch (error) { throw this._formatError(error); } } /** * Updates a customer */ async updateCustomer(customerId, customerData, onUnauthorizedCallback = null) { try { const authenticatedAxios = createAuthenticatedAxios( this.baseURL, this.tokenStorage, onUnauthorizedCallback ); let decamelizedData = decamelizeKeys(customerData); decamelizedData.id = customerId; const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId); const response = await authenticatedAxios.put(url, decamelizedData); return camelizeKeys(response.data); } catch (error) { throw this._formatError(error); } } /** * Deletes a customer */ async deleteCustomer(customerId, onUnauthorizedCallback = null) { try { const authenticatedAxios = createAuthenticatedAxios( this.baseURL, this.tokenStorage, onUnauthorizedCallback ); const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId); const response = await authenticatedAxios.delete(url); return camelizeKeys(response.data); } catch (error) { throw this._formatError(error); } } /** * Formats errors consistently */ _formatError(error) { let errorData = error.response?.data || error.response || error; return camelizeKeys(errorData); } } ``` ### Layer 2: Storage Layer (`src/services/Storage/`) **Purpose**: Local data persistence, caching, and state management **Responsibilities**: - localStorage operations - sessionStorage operations - In-memory caching - Cache invalidation - Data persistence strategies **Example: CustomerStorage.js** ```javascript // src/services/Storage/CustomerStorage.js export class CustomerStorage { constructor() { this.CUSTOMERS_CACHE_KEY = "MAPLEPRESS_CUSTOMERS_CACHE"; this.CUSTOMERS_TIMESTAMP_KEY = "MAPLEPRESS_CUSTOMERS_TIMESTAMP"; this.DEFAULT_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes // In-memory cache for current session this.memoryCache = { customers: null, customersTimestamp: null, isCustomersLoading: false, }; } /** * Gets customers from cache (memory first, then localStorage) */ getCustomersFromCache(maxAge = this.DEFAULT_CACHE_DURATION) { // Check memory cache first (fastest) if (this._isMemoryCacheValid(maxAge)) { console.log("Using memory cache for customers"); return this.memoryCache.customers; } // Check localStorage cache try { const cachedCustomers = localStorage.getItem(this.CUSTOMERS_CACHE_KEY); const cachedTimestamp = localStorage.getItem(this.CUSTOMERS_TIMESTAMP_KEY); if (cachedCustomers && cachedTimestamp) { const timestamp = parseInt(cachedTimestamp); const age = Date.now() - timestamp; if (age < maxAge) { const customersData = JSON.parse(cachedCustomers); // Update memory cache this.memoryCache.customers = customersData; this.memoryCache.customersTimestamp = timestamp; console.log("Using localStorage cache for customers"); return customersData; } } } catch (error) { console.error("Error reading from localStorage", error); this._clearLocalStorageCache(); } return null; } /** * Saves customers to cache (both memory and localStorage) */ saveCustomersToCache(customersData) { if (!customersData) { console.warn("Attempted to save null/undefined data"); return; } const timestamp = Date.now(); // Save to memory cache this.memoryCache.customers = customersData; this.memoryCache.customersTimestamp = timestamp; this.memoryCache.isCustomersLoading = false; // Save to localStorage try { localStorage.setItem( this.CUSTOMERS_CACHE_KEY, JSON.stringify(customersData) ); localStorage.setItem( this.CUSTOMERS_TIMESTAMP_KEY, timestamp.toString() ); console.log("Customers cached successfully", { timestamp: new Date(timestamp).toISOString(), count: customersData.results ? customersData.results.length : 0, }); } catch (error) { console.error("Error saving to localStorage", error); } } /** * Clears customers cache */ clearCustomersCache() { this.memoryCache.customers = null; this.memoryCache.customersTimestamp = null; this.memoryCache.isCustomersLoading = false; this._clearLocalStorageCache(); console.log("Customers cache cleared"); } /** * Sets loading state */ setCustomersCacheLoading(isLoading) { this.memoryCache.isCustomersLoading = isLoading; } /** * Gets loading state */ isCustomersCacheLoading() { return this.memoryCache.isCustomersLoading; } /** * Private helper methods */ _isMemoryCacheValid(maxAge) { if (!this.memoryCache.customers || !this.memoryCache.customersTimestamp) { return false; } const age = Date.now() - this.memoryCache.customersTimestamp; return age < maxAge; } _clearLocalStorageCache() { localStorage.removeItem(this.CUSTOMERS_CACHE_KEY); localStorage.removeItem(this.CUSTOMERS_TIMESTAMP_KEY); } } ``` ### Layer 3: Manager Layer (`src/services/Manager/`) **Purpose**: Business logic and orchestration **Responsibilities**: - Combining API and Storage services - Business rule implementation - Validation logic - Cache coordination - Providing clean interface to components **Example: CustomerManager.js** ```javascript // src/services/Manager/CustomerManager.js export class CustomerManager { constructor(customerAPI, customerStorage) { this.customerAPI = customerAPI; this.customerStorage = customerStorage; } /** * Gets list of customers with caching */ async getCustomers( params = {}, onUnauthorizedCallback = null, forceRefresh = false ) { try { // Check cache first unless force refresh if (!forceRefresh) { const cachedCustomers = this.customerStorage.getCustomersFromCache(); if (cachedCustomers) { return cachedCustomers; } } // Prevent multiple simultaneous requests if (this.customerStorage.isCustomersCacheLoading()) { console.log("Customers request already in progress"); return this._waitForCurrentRequest(); } this.customerStorage.setCustomersCacheLoading(true); try { // Validate parameters const validatedParams = this._validateCustomersParams(params); // Fetch from API const customersData = await this.customerAPI.getCustomers( validatedParams, onUnauthorizedCallback ); // Save to cache this.customerStorage.saveCustomersToCache(customersData); return customersData; } finally { this.customerStorage.setCustomersCacheLoading(false); } } catch (error) { this.customerStorage.setCustomersCacheLoading(false); console.error("Failed to get customers", error); throw error; } } /** * Creates a new customer with validation */ async createCustomer(customerData, onUnauthorizedCallback = null) { try { // Validate data const validationErrors = this._validateCustomerData(customerData, true); if (Object.keys(validationErrors).length > 0) { throw validationErrors; } // Call API const createdCustomer = await this.customerAPI.createCustomer( customerData, onUnauthorizedCallback ); // Clear cache since data changed this.customerStorage.clearCustomersCache(); return createdCustomer; } catch (error) { console.error("Failed to create customer", error); throw error; } } /** * Gets customer detail */ async getCustomerDetail(customerId, onUnauthorizedCallback = null) { try { const validationError = this._validateCustomerId(customerId); if (validationError) { throw validationError; } const customerData = await this.customerAPI.getCustomerDetail( customerId, onUnauthorizedCallback ); return customerData; } catch (error) { console.error("Failed to get customer detail", error); throw error; } } /** * Updates a customer */ async updateCustomer( customerId, customerData, onUnauthorizedCallback = null ) { try { // Validate ID const idError = this._validateCustomerId(customerId); if (idError) throw idError; // Validate data const validationErrors = this._validateCustomerData(customerData); if (Object.keys(validationErrors).length > 0) { throw validationErrors; } // Call API const updatedCustomer = await this.customerAPI.updateCustomer( customerId, customerData, onUnauthorizedCallback ); // Clear cache this.customerStorage.clearCustomersCache(); return updatedCustomer; } catch (error) { console.error("Failed to update customer", error); throw error; } } /** * Deletes a customer */ async deleteCustomer(customerId, onUnauthorizedCallback = null) { try { const validationError = this._validateCustomerId(customerId); if (validationError) throw validationError; const deleteResponse = await this.customerAPI.deleteCustomer( customerId, onUnauthorizedCallback ); // Clear cache this.customerStorage.clearCustomersCache(); return deleteResponse; } catch (error) { console.error("Failed to delete customer", error); throw error; } } /** * Clears cache */ clearCustomersCache() { this.customerStorage.clearCustomersCache(); } /** * Private validation methods */ _validateCustomerId(customerId) { if (!customerId || (typeof customerId !== "string" && typeof customerId !== "number")) { return { customerId: "Valid customer ID is required" }; } return null; } _validateCustomersParams(params) { const validatedParams = {}; // Validate pagination if (params.page && typeof params.page === "number" && params.page > 0) { validatedParams.page = params.page; } if (params.limit && typeof params.limit === "number" && params.limit > 0 && params.limit <= 1000) { validatedParams.limit = params.limit; } // Validate search if (params.search && typeof params.search === "string" && params.search.trim()) { validatedParams.search = params.search.trim(); } return validatedParams; } _validateCustomerData(customerData, isCreate = false) { const errors = {}; if (!customerData || typeof customerData !== "object") { errors.general = "Customer data is required"; return errors; } // Validate first name if (!customerData.firstName || !customerData.firstName.trim()) { errors.firstName = "First name is required"; } // Validate last name if (!customerData.lastName || !customerData.lastName.trim()) { errors.lastName = "Last name is required"; } // Validate email if (!customerData.email || !customerData.email.trim()) { errors.email = "Email is required"; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerData.email)) { errors.email = "Invalid email format"; } return errors; } _waitForCurrentRequest() { return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { if (!this.customerStorage.isCustomersCacheLoading()) { clearInterval(checkInterval); const cachedData = this.customerStorage.getCustomersFromCache(); if (cachedData) { resolve(cachedData); } else { reject(new Error("Request failed")); } } }, 100); setTimeout(() => { clearInterval(checkInterval); reject(new Error("Request timeout")); }, 30000); }); } } ``` --- ## Dependency Injection System The application uses a custom dependency injection container without third-party libraries. ### Service Registration (`src/services/Services.jsx`) ```javascript // src/services/Services.jsx import React, { createContext, useContext, useMemo } from "react"; import { getAPIBaseURL, API_ENDPOINTS } from "./Config/APIConfig"; // Import all Storage services import { TokenStorage } from "./Storage/TokenStorage"; import { CustomerStorage } from "./Storage/CustomerStorage"; // Import all API services import { AuthAPI } from "./API/AuthAPI"; import { CustomerAPI } from "./API/CustomerAPI"; // Import all Manager services import { AuthManager } from "./Manager/AuthManager"; import { CustomerManager } from "./Manager/CustomerManager"; /** * Service container class that manages all service instances */ class Services { constructor() { const baseURL = getAPIBaseURL(); // Storage Services this.tokenStorage = new TokenStorage(); this.customerStorage = new CustomerStorage(); // API Services (depend on tokenStorage) this.authAPI = new AuthAPI(baseURL, API_ENDPOINTS, this.tokenStorage); this.customerAPI = new CustomerAPI( baseURL, API_ENDPOINTS, this.tokenStorage ); // Manager Services (depend on API and Storage) this.authManager = new AuthManager(this.authAPI, this.tokenStorage); this.customerManager = new CustomerManager( this.customerAPI, this.customerStorage ); } } // Create React Context for DI const ServiceContext = createContext(null); /** * ServiceProvider component wraps the application */ export function ServiceProvider({ children }) { const services = useMemo(() => new Services(), []); return ( {children} ); } /** * Hook to access services in components */ export function useServices() { const context = useContext(ServiceContext); if (!context) { throw new Error("useServices must be used within ServiceProvider"); } return context; } ``` ### Using Services in Components ```javascript // Example: CustomerListPage.jsx import React, { useState, useEffect } from "react"; import { useServices } from "../services/Services"; function CustomerListPage() { const { customerManager } = useServices(); const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetchCustomers(); }, []); async function fetchCustomers() { try { setLoading(true); const data = await customerManager.getCustomers( { page: 1, limit: 20 }, handleUnauthorized ); setCustomers(data.results); } catch (error) { console.error("Failed to fetch customers:", error); } finally { setLoading(false); } } function handleUnauthorized() { // Handle unauthorized access (redirect to login) window.location.href = "/login"; } return (

Customers

{loading ? (

Loading...

) : ( )}
); } ``` --- ## Constants Management **CRITICAL RULE**: The application has ZERO TOLERANCE for magic numbers or strings. Every value must be defined as a constant. ### Constants Structure ``` src/constants/ ├── Customer.js # Customer-related constants ├── Authentication.js # Auth constants ├── Roles.js # User role constants ├── UI.js # UI-related constants ├── Order.js # Order constants ├── Event.js # Event constants ├── Financial.js # Financial constants ├── Storage.js # Storage key constants └── ... ``` ### Example: Customer Constants (`src/constants/Customer.js`) ```javascript // src/constants/Customer.js // Customer Status export const CUSTOMER_STATUS = { ACTIVE: 1, ARCHIVED: 2, }; // Customer Status Labels export const CUSTOMER_STATUS_LABELS = { [CUSTOMER_STATUS.ACTIVE]: "Active", [CUSTOMER_STATUS.ARCHIVED]: "Archived", }; // Customer Types export const CUSTOMER_TYPE_VALUES = { EDUCATIONAL: "educational", CORPORATE: "corporate", NON_PROFIT: "non_profit", GOVERNMENT: "government", }; // Customer Type Labels export const CUSTOMER_TYPES = { EDUCATIONAL: "Educational", CORPORATE: "Corporate", NON_PROFIT: "Non-Profit", GOVERNMENT: "Government", }; // Customer Type Options for Dropdowns export const CUSTOMER_TYPE_OPTIONS = [ { value: CUSTOMER_TYPE_VALUES.EDUCATIONAL, label: CUSTOMER_TYPES.EDUCATIONAL }, { value: CUSTOMER_TYPE_VALUES.CORPORATE, label: CUSTOMER_TYPES.CORPORATE }, { value: CUSTOMER_TYPE_VALUES.NON_PROFIT, label: CUSTOMER_TYPES.NON_PROFIT }, { value: CUSTOMER_TYPE_VALUES.GOVERNMENT, label: CUSTOMER_TYPES.GOVERNMENT }, ]; // Phone Types export const CUSTOMER_PHONE_TYPES = { MOBILE: 1, WORK: 2, HOME: 3, }; export const CUSTOMER_PHONE_TYPE_LABELS = { [CUSTOMER_PHONE_TYPES.MOBILE]: "Mobile", [CUSTOMER_PHONE_TYPES.WORK]: "Work", [CUSTOMER_PHONE_TYPES.HOME]: "Home", }; // Pagination export const CUSTOMERS_PER_PAGE = 20; export const DEFAULT_PAGE = 1; // Form Validation export const CUSTOMER_VALIDATION = { NAME_MIN_LENGTH: 2, NAME_MAX_LENGTH: 100, EMAIL_MAX_LENGTH: 255, PHONE_MAX_LENGTH: 20, ADDRESS_MAX_LENGTH: 500, COMMENT_MAX_LENGTH: 638, }; // Cache Configuration export const CUSTOMER_CACHE_EXPIRY = { LIST: 5 * 60 * 1000, // 5 minutes DETAIL: 10 * 60 * 1000, // 10 minutes SEARCH: 3 * 60 * 1000, // 3 minutes }; // Countries export const CUSTOMER_COUNTRIES = { CA: "CA", US: "US", MX: "MX", }; export const CUSTOMER_COUNTRY_LABELS = { [CUSTOMER_COUNTRIES.CA]: "Canada", [CUSTOMER_COUNTRIES.US]: "United States", [CUSTOMER_COUNTRIES.MX]: "Mexico", }; ``` ### Example: Authentication Constants (`src/constants/Authentication.js`) ```javascript // src/constants/Authentication.js // HTTP Status Codes export const HTTP_STATUS = { OK: 200, CREATED: 201, NO_CONTENT: 204, BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, }; // Token Types export const AUTH_TOKEN_TYPE = { JWT: "JWT", BEARER: "Bearer", }; // HTTP Headers export const HTTP_HEADERS = { CONTENT_TYPE_JSON: "application/json", ACCEPT_JSON: "application/json", CONTENT_TYPE_MULTIPART: "multipart/form-data", }; // Token Storage Keys export const TOKEN_STORAGE_KEYS = { ACCESS_TOKEN: "MAPLEPRESS_ACCESS_TOKEN", REFRESH_TOKEN: "MAPLEPRESS_REFRESH_TOKEN", USER_DATA: "MAPLEPRESS_USER_DATA", }; // Session Timeout export const SESSION_TIMEOUT = { WARNING: 5 * 60 * 1000, // 5 minutes warning LOGOUT: 15 * 60 * 1000, // 15 minutes auto logout }; ``` ### Using Constants in Components ```javascript import { CUSTOMER_STATUS, CUSTOMER_STATUS_LABELS } from "@constants/Customer"; function CustomerBadge({ status }) { const label = CUSTOMER_STATUS_LABELS[status]; const isActive = status === CUSTOMER_STATUS.ACTIVE; return ( {label} ); } ``` --- ## Component Architecture Components are organized into three main categories: ### 1. UI Components (`src/components/UI/`) Pure, stateless, reusable components with no business logic. ``` UI/ ├── Button/ │ └── Button.jsx ├── Input/ │ └── Input.jsx ├── Modal/ │ └── Modal.jsx ├── Table/ │ └── Table.jsx ├── Card/ │ └── Card.jsx ├── Alert/ │ └── Alert.jsx └── ... ``` **Example: Button Component** ```javascript // src/components/UI/Button/Button.jsx import React from "react"; import PropTypes from "prop-types"; import clsx from "clsx"; export function Button({ children, variant = "primary", size = "md", disabled = false, loading = false, onClick, type = "button", className, ...rest }) { const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"; const variantStyles = { primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500", danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50", }; const sizeStyles = { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2 text-base", lg: "px-6 py-3 text-lg", }; const disabledStyles = "opacity-50 cursor-not-allowed"; return ( ); } Button.propTypes = { children: PropTypes.node.isRequired, variant: PropTypes.oneOf(["primary", "secondary", "danger", "outline"]), size: PropTypes.oneOf(["sm", "md", "lg"]), disabled: PropTypes.bool, loading: PropTypes.bool, onClick: PropTypes.func, type: PropTypes.oneOf(["button", "submit", "reset"]), className: PropTypes.string, }; ``` ### 2. Business Components (`src/components/business/`) Domain-aware components with service integration. ```javascript // src/components/business/CustomerSelect.jsx import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { useServices } from "@services/Services"; export function CustomerSelect({ value, onChange, placeholder = "Select customer" }) { const { customerManager } = useServices(); const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetchCustomers(); }, []); async function fetchCustomers() { try { setLoading(true); const data = await customerManager.getCustomers({ limit: 100 }); setCustomers(data.results || []); } catch (error) { console.error("Failed to fetch customers:", error); } finally { setLoading(false); } } return ( ); } CustomerSelect.propTypes = { value: PropTypes.string, onChange: PropTypes.func.isRequired, placeholder: PropTypes.string, }; ``` ### 3. Layout Components (`src/components/Layout/`) Application structure components. ```javascript // src/components/Layout/Sidebar.jsx import React from "react"; import { Link, useLocation } from "react-router-dom"; import { HomeIcon, UsersIcon, DocumentIcon } from "@heroicons/react/24/outline"; export function Sidebar() { const location = useLocation(); const navigation = [ { name: "Dashboard", href: "/dashboard", icon: HomeIcon }, { name: "Customers", href: "/customers", icon: UsersIcon }, { name: "Orders", href: "/orders", icon: DocumentIcon }, ]; return ( ); } ``` --- ## Authentication & Authorization ### Authenticated Axios Instance ```javascript // src/services/Helpers/AuthenticatedAxios.js import axios from "axios"; import { camelizeKeys } from "humps"; import { API_ENDPOINTS } from "../Config/APIConfig"; import { AUTH_TOKEN_TYPE, HTTP_HEADERS, HTTP_STATUS, } from "@constants/Authentication"; export function createAuthenticatedAxios( baseURL, tokenStorage, onUnauthorizedCallback = null ) { const accessToken = tokenStorage.getAccessToken(); const authenticatedAxios = axios.create({ baseURL: baseURL, headers: { Authorization: `${AUTH_TOKEN_TYPE.JWT} ${accessToken}`, "Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON, Accept: HTTP_HEADERS.ACCEPT_JSON, }, }); // Add response interceptor for automatic token refresh authenticatedAxios.interceptors.response.use( (response) => response, async (error) => { const originalConfig = error.config; // Handle 401 unauthorized errors if (error.response?.status === HTTP_STATUS.UNAUTHORIZED) { const refreshToken = tokenStorage.getRefreshToken(); if (refreshToken) { try { // Attempt token refresh const refreshResponse = await handleTokenRefresh( baseURL, refreshToken ); if (refreshResponse?.status === HTTP_STATUS.OK) { // Save new tokens const newAccessToken = refreshResponse.data.access_token; const newRefreshToken = refreshResponse.data.refresh_token; tokenStorage.setAccessToken(newAccessToken); tokenStorage.setRefreshToken(newRefreshToken); // Retry original request with new token const retryConfig = { ...originalConfig, headers: { ...originalConfig.headers, Authorization: `${AUTH_TOKEN_TYPE.JWT} ${newAccessToken}`, }, }; return authenticatedAxios(retryConfig); } } catch (refreshError) { console.error("Token refresh failed:", refreshError); if (onUnauthorizedCallback) { onUnauthorizedCallback(); } } } else if (onUnauthorizedCallback) { onUnauthorizedCallback(); } } return Promise.reject(error.response?.data || error); } ); return authenticatedAxios; } async function handleTokenRefresh(baseURL, refreshToken) { const refreshAxios = axios.create({ baseURL: baseURL, headers: { "Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON, Accept: HTTP_HEADERS.ACCEPT_JSON, Authorization: `${AUTH_TOKEN_TYPE.BEARER} ${refreshToken}`, }, }); const refreshData = { value: refreshToken }; return await refreshAxios.post(API_ENDPOINTS.REFRESH_TOKEN, refreshData); } ``` ### Token Storage ```javascript // src/services/Storage/TokenStorage.js import { TOKEN_STORAGE_KEYS } from "@constants/Authentication"; export class TokenStorage { /** * Gets access token from localStorage */ getAccessToken() { return localStorage.getItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN); } /** * Sets access token in localStorage */ setAccessToken(token) { if (token) { localStorage.setItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN, token); } else { localStorage.removeItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN); } } /** * Gets refresh token from localStorage */ getRefreshToken() { return localStorage.getItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN); } /** * Sets refresh token in localStorage */ setRefreshToken(token) { if (token) { localStorage.setItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN, token); } else { localStorage.removeItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN); } } /** * Clears all tokens */ clearTokens() { localStorage.removeItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN); localStorage.removeItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN); localStorage.removeItem(TOKEN_STORAGE_KEYS.USER_DATA); } /** * Checks if user is authenticated */ isAuthenticated() { return !!this.getAccessToken(); } } ``` --- ## Routing & Navigation ### Main Router (`src/AppRouter.jsx`) ```javascript // src/AppRouter.jsx import React from "react"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { useServices } from "./services/Services"; // Pages import LoginPage from "./pages/Auth/Login/Page"; import DashboardPage from "./pages/Dashboard/Page"; import CustomerListPage from "./pages/Admin/Customer/List/Page"; import CustomerDetailPage from "./pages/Admin/Customer/Detail/Page"; import CustomerCreatePage from "./pages/Admin/Customer/Create/Page"; import CustomerUpdatePage from "./pages/Admin/Customer/Update/Page"; function AppRouter() { return ( {/* Public routes */} } /> {/* Protected routes */} } /> {/* Customer routes */} } /> } /> } /> } /> {/* Default redirect */} } /> ); } function ProtectedRoute({ children }) { const { authManager } = useServices(); if (!authManager.isAuthenticated()) { return ; } return children; } export default AppRouter; ``` --- ## State Management & Caching ### Caching Strategy The application uses a two-tier caching strategy: 1. **Memory Cache**: Fastest, cleared on page reload 2. **localStorage Cache**: Persistent across sessions ### Example: List Page with Caching ```javascript // src/pages/Admin/Customer/List/Page.jsx import React, { useState, useEffect } from "react"; import { useServices } from "@services/Services"; import { CUSTOMERS_PER_PAGE } from "@constants/Customer"; export default function CustomerListPage() { const { customerManager } = useServices(); const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [totalCount, setTotalCount] = useState(0); useEffect(() => { fetchCustomers(); }, [page]); async function fetchCustomers(forceRefresh = false) { try { setLoading(true); const data = await customerManager.getCustomers( { page: page, limit: CUSTOMERS_PER_PAGE, }, handleUnauthorized, forceRefresh ); setCustomers(data.results || []); setTotalCount(data.count || 0); } catch (error) { console.error("Failed to fetch customers:", error); // Show error toast } finally { setLoading(false); } } function handleUnauthorized() { window.location.href = "/login"; } function handleRefresh() { fetchCustomers(true); // Force refresh } return (

Customers

{loading ? ( ) : ( <> )}
); } ``` --- ## Styling & Theming ### Tailwind CSS Configuration ```javascript // tailwind.config.js export default { content: [ "./index.html", "./src/**/*.{js,jsx}", ], theme: { extend: { screens: { 'xs': '400px', }, colors: { primary: { 50: '#eff6ff', 100: '#dbeafe', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', }, }, }, }, plugins: [], } ``` ### Global Styles (`src/styles/app.css`) ```css @import "tailwindcss"; /* Base styles */ body { @apply bg-gray-50 text-gray-900; } /* Utility classes */ .btn { @apply px-4 py-2 rounded-md font-medium transition-colors; } .btn-primary { @apply btn bg-blue-600 text-white hover:bg-blue-700; } .btn-secondary { @apply btn bg-gray-600 text-white hover:bg-gray-700; } .form-input { @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm; @apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent; } .form-select { @apply form-input; } ``` --- ## Build Configuration ### Vite Configuration (`vite.config.js`) ```javascript // vite.config.js import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; export default defineConfig(({ mode }) => ({ plugins: [ react(), tailwindcss(), ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), "@components": path.resolve(__dirname, "./src/components"), "@pages": path.resolve(__dirname, "./src/pages"), "@services": path.resolve(__dirname, "./src/services"), "@constants": path.resolve(__dirname, "./src/constants"), "@utils": path.resolve(__dirname, "./src/utils"), "@styles": path.resolve(__dirname, "./src/styles"), }, }, server: { port: 3000, open: true, cors: true, proxy: { "/api": { target: process.env.VITE_API_URL || "http://localhost:8080", changeOrigin: true, secure: false, }, }, }, build: { outDir: "dist", sourcemap: mode === "development" || mode === "staging", minify: "terser", terserOptions: { compress: { drop_console: mode === "production" || mode === "staging", drop_debugger: true, }, }, rollupOptions: { output: { manualChunks: { "react-vendor": ["react", "react-dom", "react-router-dom"], "ui-vendor": ["@heroicons/react", "clsx"], "utils-vendor": ["axios", "luxon", "humps"], }, }, }, chunkSizeWarningLimit: 1000, }, optimizeDeps: { include: [ "react", "react-dom", "react-router-dom", "@heroicons/react/24/outline", "@heroicons/react/24/solid", ], }, })); ``` --- ## Complete Setup Guide ### Step 1: Initialize Project ```bash # Create new React project with Vite npm create vite@latest my-app -- --template react cd my-app # Install dependencies npm install react@^19.1.0 react-dom@^19.1.0 npm install react-router-dom@^7.8.0 npm install axios@^1.11.0 npm install luxon@^3.7.1 npm install humps@^2.0.1 npm install @heroicons/react@^2.2.0 npm install clsx@^2.1.1 npm install prop-types@^15.8.1 # Install dev dependencies npm install -D tailwindcss@^4.1.12 npm install -D @tailwindcss/vite@^4.1.11 npm install -D @tailwindcss/postcss@^4.1.12 npm install -D vite@^7.1.2 npm install -D @vitejs/plugin-react@^4.7.0 npm install -D terser@^5.44.0 npm install -D eslint@^9.30.1 npm install -D @eslint/js@^9.30.1 npm install -D eslint-plugin-react-hooks@^5.2.0 ``` ### Step 2: Configure Vite Create `vite.config.js` as shown in Build Configuration section. ### Step 3: Configure Tailwind Create `tailwind.config.js` as shown in Styling section. ### Step 4: Create Directory Structure ```bash mkdir -p src/{components/{UI,UIX,business,Layout},pages,services/{API,Storage,Manager,Config,Helpers},constants,utils,styles,assets} ``` ### Step 5: Create Service Layer 1. **Create API Config** ```javascript // src/services/Config/APIConfig.js export function getAPIBaseURL() { return import.meta.env.VITE_API_URL || "http://localhost:8080"; } export const API_ENDPOINTS = { // Auth LOGIN: "/api/v1/auth/login", LOGOUT: "/api/v1/auth/logout", REFRESH_TOKEN: "/api/v1/auth/refresh", // Customers CUSTOMERS: "/api/v1/customers", CUSTOMER_DETAIL: "/api/v1/customers/{id}", CUSTOMER_COUNT: "/api/v1/customers/count", }; export const ENV_CONFIG = { API_URL: import.meta.env.VITE_API_URL, ENV: import.meta.env.MODE, }; ``` 2. **Create Storage Services** (TokenStorage, CustomerStorage, etc.) 3. **Create API Services** (AuthAPI, CustomerAPI, etc.) 4. **Create Manager Services** (AuthManager, CustomerManager, etc.) 5. **Create Services Container** (`src/services/Services.jsx`) ### Step 6: Create Constants Create constant files for each domain (Customer, Auth, UI, etc.) as shown in Constants Management section. ### Step 7: Create Components 1. Create UI components (Button, Input, Modal, etc.) 2. Create Layout components (Sidebar, Header, Footer) 3. Create business components (CustomerSelect, etc.) ### Step 8: Create Pages Create page components following the structure: ``` pages/ └── Admin/ └── Customer/ ├── List/ │ └── Page.jsx ├── Detail/ │ └── Page.jsx ├── Create/ │ └── Page.jsx └── Update/ └── Page.jsx ``` ### Step 9: Setup Router Create `AppRouter.jsx` as shown in Routing section. ### Step 10: Setup Main Entry ```javascript // src/main.jsx import React from "react"; import ReactDOM from "react-dom/client"; import App from "./AppRouter"; import { ServiceProvider } from "./services/Services"; import "./styles/app.css"; // Error Boundary class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error("Error:", error, errorInfo); } render() { if (this.state.hasError) { return (

Something went wrong

); } return this.props.children; } } const root = ReactDOM.createRoot(document.getElementById("root")); root.render( ); ``` ### Step 11: Environment Variables Create `.env` files: ```bash # .env.development VITE_API_URL=http://localhost:8080 # .env.production VITE_API_URL=https://api.production.com # .env.staging VITE_API_URL=https://api.staging.com ``` ### Step 12: Package.json Scripts ```json { "scripts": { "dev": "vite", "build": "vite build", "build:production": "vite build --mode production", "build:staging": "vite build --mode staging", "preview": "vite preview", "lint": "eslint .", "clean": "rm -rf node_modules dist", "clean:install": "npm run clean && npm install" } } ``` --- ## Best Practices & Patterns ### 1. Service Layer Rules - **Components ONLY interact with Manager services** - **Never call API services directly from components** - **Never access Storage services directly from components** - **Managers orchestrate API and Storage layers** ### 2. Constants Rules - **ZERO TOLERANCE for magic values** - **Every number, string, or config value must be a constant** - **Check `src/constants/` before writing ANY code** - **Create new constants if they don't exist** ### 3. Component Rules - **UI components are pure and stateless** - **Business components use the service layer** - **Page components handle routing logic** - **Always use PropTypes** ### 4. Error Handling ```javascript // Consistent error handling pattern try { const data = await manager.getData(); setData(data); } catch (error) { console.error("Operation failed:", error); // Show user-friendly error message showToast("Failed to load data", "error"); } finally { setLoading(false); } ``` ### 5. Caching Strategy - **Use memory cache first (fastest)** - **Fall back to localStorage** - **Clear cache on mutations** - **Implement loading flags to prevent duplicate requests** ### 6. Code Organization ```javascript // Component structure order: // 1. Imports // 2. Component definition // 3. State hooks // 4. Effect hooks // 5. Event handlers // 6. Helper functions // 7. Render logic // 8. PropTypes ``` ### 7. Naming Conventions - **Components**: PascalCase (CustomerList) - **Functions**: camelCase (fetchCustomers) - **Constants**: UPPER_SNAKE_CASE (CUSTOMER_STATUS) - **CSS Classes**: kebab-case (customer-list) - **Files**: PascalCase for components, camelCase for utilities ### 8. Testing Checklist Before deployment, verify: - [ ] All magic values replaced with constants - [ ] Components use Manager services only - [ ] Error handling implemented - [ ] Loading states handled - [ ] Caching working correctly - [ ] Authentication flow tested - [ ] Token refresh working - [ ] PropTypes defined - [ ] Console logs removed (in production) - [ ] Build succeeds without warnings --- ## Conclusion This architecture provides: - **Scalability**: Clear separation of concerns allows teams to work independently - **Maintainability**: Consistent patterns make code easy to understand and modify - **Testability**: Service layer isolation enables comprehensive testing - **Performance**: Multi-tier caching reduces API calls - **Reliability**: Automatic token refresh and error handling - **Developer Experience**: Path aliases, constants, and clear patterns Follow these patterns strictly to maintain consistency and quality across the entire application. --- **Document Version**: 1.0.0 **Last Updated**: 2025-01-30 **Maintained By**: Development Team