Skip to main content

State Management

The Iquea frontend uses React Context API for global state management, providing a lightweight alternative to Redux or other state management libraries.

State Architecture

The application has two primary context providers:

AuthContext

Manages user authentication state with JWT tokens

CartContext

Manages shopping cart items with localStorage persistence

AuthContext

Handles user authentication state, JWT token management, and role-based access.

Implementation

src/context/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
import { jwtDecode } from 'jwt-decode';

interface JwtPayload {
    sub: string;   // email
    rol: string;
    exp: number;
}

interface AuthContextType {
    token: string | null;
    email: string | null;
    rol: string | null;
    isAuthenticated: boolean;
    setToken: (token: string | null) => void;
    logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
    const [token, setTokenState] = useState<string | null>(
        () => localStorage.getItem('token')
    );

    const decoded = token ? jwtDecode<JwtPayload>(token) : null;
    const email = decoded?.sub ?? null;
    const rol = decoded?.rol ?? null;
    const isAuthenticated = !!token && (decoded?.exp ?? 0) > Date.now() / 1000;

    function setToken(newToken: string | null) {
        if (newToken) {
            localStorage.setItem('token', newToken);
        } else {
            localStorage.removeItem('token');
        }
        setTokenState(newToken);
    }

    function logout() {
        setToken(null);
    }

    // Clean up expired token on mount
    useEffect(() => {
        if (token && (decoded?.exp ?? 0) < Date.now() / 1000) {
            logout();
        }
    }, []);

    return (
        <AuthContext.Provider value={{ token, email, rol, isAuthenticated, setToken, logout }}>
            {children}
        </AuthContext.Provider>
    );
}

export function useAuth() {
    const ctx = useContext(AuthContext);
    if (!ctx) throw new Error('useAuth must be used within AuthProvider');
    return ctx;
}

Context API

token
string | null
The raw JWT token string from localStorage
email
string | null
User’s email extracted from JWT payload (sub claim)
rol
string | null
User’s role (e.g., “ADMIN” or “CLIENTE”) from JWT payload
isAuthenticated
boolean
true if token exists and is not expired
setToken
(token: string | null) => void
Sets the JWT token in state and localStorage
logout
() => void
Removes token from state and localStorage

Usage Examples

src/pages/Login.tsx
import { useAuth } from '../context/AuthContext';
import { login } from '../api/auth';
import { useNavigate } from 'react-router-dom';

function Login() {
  const { setToken } = useAuth();
  const navigate = useNavigate();
  const [credentials, setCredentials] = useState({ email: '', password: '' });

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    try {
      const { token } = await login(credentials);
      setToken(token);  // Save token to context and localStorage
      navigate('/');    // Redirect to home
    } catch (error) {
      console.error('Login failed:', error);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
    </form>
  );
}

JWT Token Management

The AuthContext uses jwt-decode to extract claims from the JWT:
import { jwtDecode } from 'jwt-decode';

interface JwtPayload {
    sub: string;   // Email (subject)
    rol: string;   // User role
    exp: number;   // Expiration timestamp
}

const decoded = token ? jwtDecode<JwtPayload>(token) : null;
const isAuthenticated = !!token && (decoded?.exp ?? 0) > Date.now() / 1000;
Token Expiration: The context automatically checks if the token is expired and clears it on mount. However, it doesn’t actively monitor expiration during the session. Consider adding periodic checks or handling 401 responses.

CartContext

Manages shopping cart state with localStorage persistence.

Implementation

src/context/CartContext.tsx
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import type { Producto, CartItem } from '../types';

interface CartContextType {
    cart: CartItem[];
    addToCart: (product: Producto, quantity: number) => void;
    removeFromCart: (productId: number) => void;
    updateQuantity: (productId: number, quantity: number) => void;
    clearCart: () => void;
    total: number;
    count: number;
}

const CartContext = createContext<CartContextType | undefined>(undefined);

export function CartProvider({ children }: { children: ReactNode }) {
    const [cart, setCart] = useState<CartItem[]>(() => {
        const saved = localStorage.getItem('cart');
        return saved ? JSON.parse(saved) : [];
    });

    // Persist to localStorage on every change
    useEffect(() => {
        localStorage.setItem('cart', JSON.stringify(cart));
    }, [cart]);

    const addToCart = (product: Producto, quantity: number) => {
        setCart((prev) => {
            const existing = prev.find((item) => item.producto.producto_id === product.producto_id);
            if (existing) {
                // Update quantity if product already in cart
                return prev.map((item) =>
                    item.producto.producto_id === product.producto_id
                        ? { ...item, cantidad: item.cantidad + quantity }
                        : item
                );
            }
            // Add new product to cart
            return [...prev, { producto: product, cantidad: quantity }];
        });
    };

    const removeFromCart = (productId: number) => {
        setCart((prev) => prev.filter((item) => item.producto.producto_id !== productId));
    };

    const updateQuantity = (productId: number, quantity: number) => {
        if (quantity <= 0) {
            removeFromCart(productId);
            return;
        }
        setCart((prev) =>
            prev.map((item) =>
                item.producto.producto_id === productId ? { ...item, cantidad: quantity } : item
            )
        );
    };

    const clearCart = () => setCart([]);

    const total = cart.reduce((sum, item) => sum + (item.producto.precioCantidad * item.cantidad), 0);
    const count = cart.reduce((sum, item) => sum + item.cantidad, 0);

    return (
        <CartContext.Provider value={{ cart, addToCart, removeFromCart, updateQuantity, clearCart, total, count }}>
            {children}
        </CartContext.Provider>
    );
}

export function useCart() {
    const context = useContext(CartContext);
    if (context === undefined) {
        throw new Error('useCart must be used within a CartProvider');
    }
    return context;
}

Context API

cart
CartItem[]
Array of cart items with product and quantity
addToCart
(product: Producto, quantity: number) => void
Adds a product to cart or increments quantity if already present
removeFromCart
(productId: number) => void
Removes a product from cart by ID
updateQuantity
(productId: number, quantity: number) => void
Updates product quantity. Removes item if quantity ≤ 0
clearCart
() => void
Removes all items from cart
total
number
Total cart value (sum of all items × quantity)
count
number
Total number of items in cart

Type Definitions

src/types/index.ts
export interface CartItem {
    producto: Producto;
    cantidad: number;
}

export interface Producto {
    producto_id: number;
    sku: string;
    nombre: string;
    descripcion: string;
    precioCantidad: number;
    precioMoneda: string;
    dimensionesAlto: number;
    dimensionesAncho: number;
    dimensionesProfundo: number;
    es_destacado: boolean;
    stock: number;
    imagen_url: string;
    categoria: CategoriaResumen;
}

Usage Examples

src/pages/ProductDetail.tsx
import { useCart } from '../context/CartContext';
import { useState } from 'react';

function ProductDetail({ producto }: { producto: Producto }) {
  const { addToCart } = useCart();
  const [quantity, setQuantity] = useState(1);

  function handleAddToCart() {
    addToCart(producto, quantity);
    // Show success message
  }

  return (
    <div>
      <h1>{producto.nombre}</h1>
      <input
        type="number"
        min="1"
        max={producto.stock}
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
      />
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}

LocalStorage Persistence

The cart automatically persists to localStorage:
// Initialize from localStorage
const [cart, setCart] = useState<CartItem[]>(() => {
    const saved = localStorage.getItem('cart');
    return saved ? JSON.parse(saved) : [];
});

// Save to localStorage on every change
useEffect(() => {
    localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
This provides:
  • Persistence - Cart survives page refreshes
  • Cross-tab sync - Cart updates across browser tabs (with additional event listeners)
  • Offline support - Cart data available without network

Context Provider Hierarchy

The contexts are nested in App.tsx to ensure proper availability:
<AuthProvider>           {/* Outermost - available everywhere */}
  <CartProvider>         {/* Can access AuthContext if needed */}
    <BrowserRouter>
      {/* All routes have access to both contexts */}
    </BrowserRouter>
  </CartProvider>
</AuthProvider>

Best Practices

Keep contexts focused on a single concern:
  • AuthContext handles authentication only
  • CartContext handles cart operations only
  • Avoid creating one mega-context
Both contexts throw errors if used outside their providers:
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}
This prevents undefined context bugs.
All context values are strictly typed:
interface AuthContextType {
  token: string | null;
  email: string | null;
  // ...
}
TypeScript ensures correct usage throughout the app.
  • Use separate contexts to avoid unnecessary re-renders
  • Cart calculations (total, count) are computed values, not stored state
  • Consider memoization for expensive computations

Next Steps

API Integration

Learn how the frontend communicates with the backend API using authenticated requests

Build docs developers (and LLMs) love