Modern React E-Commerce Architecture
This guide documents the architectural patterns used in the “Ecommerce” project. It features a modern frontend stack using Vite, TypeScript, Firebase Authentication, and TanStack Query.
Table of Contents
- Tech Stack & Dependencies
- Project Structure
- State Management (Context + Reducer)
- Data Fetching (TanStack Query)
- Authentication (Firebase)
- CI/CD Workflow
Tech Stack & Dependencies
- Build Tool: Vite (
npm create vite@latest) - Language: TypeScript
- Routing: React Router DOM v6+
- State: React Context API +
useReducer - Server State: TanStack Query (
@tanstack/react-query) - Auth: Firebase v9+ (Modular SDK)
- HTTP Client: Axios
- Testing: Jest, React Testing Library
Project Structure
src/
├── api/ # Axios instances and API calls
├── components/ # Reusable UI components (ProductCard, Navbar)
├── context/ # Global state (Auth, Products)
├── lib/ # Third-party config (Firebase)
├── pages/ # Route views (Home, Login, Profile)
├── types/ # TypeScript interfaces
└── main.tsx # Entry point
State Management (Context + Reducer)
The project uses the Context API combined with useReducer for complex state logic (like managing a product list with filters).
Type Definitions (types.ts)
export interface Product {
id: number;
title: string;
price: number;
category: string;
image: string;
}
export interface ProductState {
products: Product[];
selectedCategory: string;
}
The Provider Pattern (context/ProductContext.tsx)
// 1. Define Actions
type ProductAction =
| { type: 'SET_PRODUCTS'; payload: Product[] }
| { type: 'SET_SELECTED_CATEGORY'; payload: string };
// 2. Create Reducer
const productReducer = (state: ProductState, action: ProductAction): ProductState => {
switch (action.type) {
case 'SET_PRODUCTS':
return { ...state, products: action.payload };
case 'SET_SELECTED_CATEGORY':
return { ...state, selectedCategory: action.payload };
default:
throw new Error(`Unhandled action type`);
}
};
// 3. Create Provider & Custom Hook
export const ProductProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(productReducer, initialState);
return (
<ProductContext.Provider value=>
{children}
</ProductContext.Provider>
);
};
export const useProductContext = () => {
const context = useContext(ProductContext);
if (!context) throw new Error("Must be used within ProductProvider");
return context;
}
Data Fetching (TanStack Query)
Instead of useEffect fetching, the project uses TanStack Query for caching, loading states, and error handling.
API Layer (api/api.ts)
import axios from "axios";
const apiClient = axios.create({ baseURL: 'https://fakestoreapi.com' });
export const fetchProducts = () => apiClient.get<Product[]>('/products');
Usage in Components (pages/Home.tsx)
const { data, isLoading, isError } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
// Automatic loading/error handling
if (isLoading) return <h1>Loading...</h1>;
if (isError) return <h2>Error loading products</h2>;
Authentication (Firebase)
Authentication is handled via a dedicated AuthenticationContext that listens to Firebase’s onAuthStateChanged.
Setup (lib/firebase/firebase.ts)
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = { /* ... */ };
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
Auth Context Logic
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user ? user : null);
});
return () => unsubscribe(); // Cleanup subscription on unmount
}, []);
CI/CD Workflow
The project includes a GitHub Actions workflow (.github/workflows/ci-cd.yml) for automated testing and deployment.
Triggers: Push or Pull Request to main.
Jobs:
- Build:
- Checkout code
- Install dependencies (
npm install) - Run tests (
npm test) - Build app (
npm run build)
- Deploy:
- Runs only if Build succeeds.
- Deploys to Vercel using the Vercel CLI.
7. Form Validation (Formik & Yup)
E-commerce applications require robust form handling for checkouts and user profiles. This project compares manual validation against industrial libraries.
The “Old Way” (Manual State)
Requires managing separate state for values and errors, plus a manual validation function.
const [values, setValues] = useState({ email: '' });
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!values.email.includes('@')) newErrors.email = "Invalid email";
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length === 0) {
// Submit data
} else {
setErrors(validationErrors);
}
};
The “Industrial Way” (Formik + Yup)
Using Formik for lifecycle and Yup for schema-based validation.
import { useFormik } from 'formik';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().email('Invalid email').required('Required'),
password: Yup.string().min(6, 'Too short').required('Required'),
});
const SignupForm = () => {
const formik = useFormik({
initialValues: { email: '', password: '' },
validationSchema,
onSubmit: values => console.log(values),
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
onChange={formik.handleChange}
value={formik.values.email}
/>
{formik.errors.email && <div>{formik.errors.email}</div>}
<button type="submit">Submit</button>
</form>
);
};
See Also
- React Basics Guide - Components and State.
- Modern Fullstack Guide - Next.js and Firebase Integration.
- CI/CD Pipeline Guide - Deployment automation.