Full Stack Learning Hub

Comprehensive guides, cheat sheets, and code examples for full stack development.

View on GitHub

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

  1. Tech Stack & Dependencies
  2. Project Structure
  3. State Management (Context + Reducer)
  4. Data Fetching (TanStack Query)
  5. Authentication (Firebase)
  6. CI/CD Workflow

Tech Stack & Dependencies


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:

  1. Build:
    • Checkout code
    • Install dependencies (npm install)
    • Run tests (npm test)
    • Build app (npm run build)
  2. 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