Frontend Architecture: Building Scalable React Applications
Frontend architecture is crucial for building maintainable and scalable applications. Let's explore essential patterns and best practices for React applications.
Project Structure
1. Feature-Based Architecture
Organize code by features:
// src/features/auth/types/index.ts export interface User { id: string; email: string; name: string; role: UserRole; } export enum UserRole { ADMIN = "ADMIN", USER = "USER", } // src/features/auth/api/auth.api.ts import { api } from "@/lib/api"; import { User } from "../types"; export const authApi = { login: async (credentials: LoginCredentials): Promise<AuthResponse> => { const response = await api.post("/auth/login", credentials); return response.data; }, register: async (userData: RegisterData): Promise<User> => { const response = await api.post("/auth/register", userData); return response.data; }, getCurrentUser: async (): Promise<User> => { const response = await api.get("/auth/me"); return response.data; }, }; // src/features/auth/components/LoginForm.tsx import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { loginSchema } from "../schemas"; import { useAuth } from "../hooks/useAuth"; export const LoginForm = () => { const { login, isLoading } = useAuth(); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(loginSchema), }); const onSubmit = async (data: LoginFormData) => { await login(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <Input {...register("email")} error={errors.email?.message} label="Email" /> <Input {...register("password")} type="password" error={errors.password?.message} label="Password" /> <Button type="submit" loading={isLoading}> Login </Button> </form> ); };
State Management
1. React Query Configuration
Set up React Query for server state:
// src/lib/react-query.ts import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, cacheTime: 10 * 60 * 1000, retry: 1, refetchOnWindowFocus: false, }, }, }); // src/features/posts/hooks/usePosts.ts import { useQuery, useMutation } from "@tanstack/react-query"; import { postsApi } from "../api/posts.api"; export const usePosts = (params: PostsParams) => { return useQuery({ queryKey: ["posts", params], queryFn: () => postsApi.getPosts(params), keepPreviousData: true, }); }; export const useCreatePost = () => { return useMutation({ mutationFn: postsApi.createPost, onSuccess: () => { queryClient.invalidateQueries(["posts"]); }, }); };
2. Zustand Store
Implement client state management:
// src/stores/theme.store.ts import create from "zustand"; import { persist } from "zustand/middleware"; interface ThemeState { theme: "light" | "dark"; setTheme: (theme: "light" | "dark") => void; toggleTheme: () => void; } export const useThemeStore = create<ThemeState>()( persist( (set) => ({ theme: "light", setTheme: (theme) => set({ theme }), toggleTheme: () => set((state) => ({ theme: state.theme === "light" ? "dark" : "light", })), }), { name: "theme-storage", } ) ); // src/stores/auth.store.ts interface AuthState { user: User | null; setUser: (user: User | null) => void; isAuthenticated: boolean; } export const useAuthStore = create<AuthState>((set) => ({ user: null, isAuthenticated: false, setUser: (user) => set({ user, isAuthenticated: !!user, }), }));
Component Architecture
1. Component Composition
Implement component composition patterns:
// src/components/layout/AppLayout.tsx interface AppLayoutProps { header?: React.ReactNode; sidebar?: React.ReactNode; children: React.ReactNode; footer?: React.ReactNode; } export const AppLayout = ({ header, sidebar, children, footer, }: AppLayoutProps) => { return ( <div className="min-h-screen bg-gray-50"> {header && <header className="sticky top-0 z-10">{header}</header>} <div className="flex"> {sidebar && <aside className="w-64 min-h-screen">{sidebar}</aside>} <main className="flex-1 p-6">{children}</main> </div> {footer && <footer>{footer}</footer>} </div> ); }; // src/components/data-display/DataTable.tsx interface DataTableProps<T> { data: T[]; columns: Column<T>[]; isLoading?: boolean; onRowClick?: (item: T) => void; renderEmpty?: () => React.ReactNode; renderError?: (error: Error) => React.ReactNode; } export const DataTable = <T extends object>({ data, columns, isLoading, onRowClick, renderEmpty, renderError, }: DataTableProps<T>) => { if (isLoading) { return <TableSkeleton columns={columns.length} />; } if (!data.length && renderEmpty) { return renderEmpty(); } return ( <table className="min-w-full divide-y"> <thead> <tr> {columns.map((column) => ( <th key={column.key}>{column.title}</th> ))} </tr> </thead> <tbody> {data.map((item, index) => ( <tr key={index} onClick={() => onRowClick?.(item)} className={clsx("hover:bg-gray-50", onRowClick && "cursor-pointer")} > {columns.map((column) => ( <td key={column.key}> {column.render ? column.render(item) : item[column.key as keyof T]} </td> ))} </tr> ))} </tbody> </table> ); };
API Layer
1. API Client Configuration
Set up API client with interceptors:
// src/lib/api.ts import axios from "axios"; import { toast } from "react-hot-toast"; export const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, headers: { "Content-Type": "application/json", }, }); api.interceptors.request.use((config) => { const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); api.interceptors.response.use( (response) => response, (error) => { const message = error.response?.data?.message || "An error occurred"; toast.error(message); if (error.response?.status === 401) { // Handle unauthorized } return Promise.reject(error); } ); // src/lib/api-hooks.ts export const createApiHook = <T, P = void>( apiCall: (params: P) => Promise<T> ) => { return (params: P) => useQuery({ queryKey: [apiCall.name, params], queryFn: () => apiCall(params), }); }; export const createMutationHook = <T, P = void>( apiCall: (params: P) => Promise<T>, options?: UseMutationOptions<T, Error, P> ) => { return () => useMutation({ mutationFn: apiCall, ...options, }); };
Performance Optimization
1. Code Splitting
Implement dynamic imports:
// src/pages/dashboard.tsx import dynamic from "next/dynamic"; const DashboardChart = dynamic( () => import("@/features/dashboard/components/DashboardChart"), { loading: () => <ChartSkeleton />, ssr: false, } ); const DashboardStats = dynamic( () => import("@/features/dashboard/components/DashboardStats") ); export default function DashboardPage() { return ( <div className="space-y-6"> <DashboardStats /> <DashboardChart /> </div> ); } // src/components/common/Image.tsx import { useState } from "react"; import NextImage from "next/image"; export const Image = (props: ImageProps) => { const [isLoading, setLoading] = useState(true); return ( <div className={clsx("relative", isLoading && "animate-pulse bg-gray-200")}> <NextImage {...props} onLoadingComplete={() => setLoading(false)} /> </div> ); };
Error Handling
1. Error Boundary Implementation
Create error boundaries:
// src/components/error/ErrorBoundary.tsx import { Component, ErrorInfo } from "react"; interface Props { children: React.ReactNode; fallback?: React.ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; } interface State { hasError: boolean; error?: Error; } export class ErrorBoundary extends Component<Props, State> { state: State = { hasError: false, }; static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { this.props.onError?.(error, errorInfo); } render() { if (this.state.hasError) { return ( this.props.fallback || ( <div className="p-4 bg-red-50 text-red-700"> <h2>Something went wrong</h2> <pre>{this.state.error?.message}</pre> </div> ) ); } return this.props.children; } } // Usage const App = () => { return ( <ErrorBoundary onError={(error, errorInfo) => { // Log error to service }} > <AppContent /> </ErrorBoundary> ); };
Best Practices
- Feature-Based Structure: Organize code by features
- Component Composition: Use composition over inheritance
- State Management: Separate server and client state
- Performance: Implement code splitting and lazy loading
- Error Handling: Use error boundaries and proper error handling
- Type Safety: Leverage TypeScript for better type safety
- Testing: Write comprehensive tests for components
- Documentation: Maintain clear documentation
Implementation Checklist
- Set up project structure
- Configure state management
- Implement component architecture
- Set up API layer
- Add performance optimizations
- Implement error handling
- Add TypeScript configurations
- Set up testing framework
Conclusion
A well-structured frontend architecture is essential for building maintainable React applications. Focus on implementing these patterns and practices to create scalable applications.