Implementación de Catálogo de Productos y Carrito

Video
30 min~11 min lectura
Objetivo de la lección

El catálogo es la vitrina digital, la representación estructurada y atractiva de todos los artículos disponibles para la venta.

Puntos de control
  • Fundamentos del Catálogo y el Estado del Carrito
  • Concepto Clave: Estado Global y Flujo de Datos Unidireccional
  • Cómo Funciona en la Práctica: Arquitectura Paso a Paso
  • Código en Acción: Store del Carrito y Componente de Producto

Reproductor de video

Fundamentos del Catálogo y el Estado del Carrito

En el núcleo de cualquier aplicación de comercio electrónico se encuentran dos componentes esenciales: el catálogo de productos y el carrito de compras. El catálogo es la vitrina digital, la representación estructurada y atractiva de todos los artículos disponibles para la venta. Su implementación no se limita a mostrar una lista de imágenes y textos; implica una arquitectura de datos eficiente, una gestión de estado para el filtrado y la paginación, y una interfaz de usuario que invite a la interacción. En React Native, esto se traduce en componentes como FlatList o FlashList (de Shopify) para un rendimiento óptimo con grandes volúmenes de datos, tarjetas de producto personalizadas y una navegación fluida hacia los detalles de cada ítem.

Por otro lado, el carrito de compras es el centro de operaciones del usuario. Es un estado global complejo que debe ser persistente, accesible desde cualquier pantalla de la aplicación y reactivo a los cambios. Aquí es donde conceptos como Context API, Zustand o Redux Toolkit demuestran su valor. El estado del carrito no solo almacena IDs y cantidades; debe manejar lógica de negocio como la validación de inventario, el cálculo de subtotales, impuestos, envíos y descuentos aplicados. La sincronización en tiempo real entre la vista del catálogo (donde se añaden productos) y el ícono del carrito (que muestra la cantidad total de ítems) es un detalle de UX crítico que depende de una gestión de estado bien diseñada.

La interconexión entre ambos sistemas es lo que define la experiencia de compra. Un usuario debe poder explorar el catálogo, añadir o eliminar productos del carrito, modificar cantidades y ver un resumen actualizado al instante, todo sin bloqueos o inconsistencias en los datos. Esta lección se enfoca en construir esta dualidad de manera robusta, escalable y mantenible, utilizando las mejores prácticas de React Native y Expo.

Concepto Clave: Estado Global y Flujo de Datos Unidireccional

Imagina el carrito de compras como un contenedor centralizado en un almacén. En lugar de que cada empleado (componente de la app) lleve su propia lista de lo que el cliente va a comprar, todos consultan y modifican una única lista maestra ubicada en un lugar de acceso común. Este "lugar común" es el Estado Global. Cuando un componente de producto en el catálogo necesita añadir un ítem, no lo hace directamente en su memoria local, sino que envía una solicitud ("dispatch") al gestor del almacén (el "store" o "context"). Este gestor actualiza la lista maestra y, automáticamente, notifica a todos los componentes interesados (como el ícono del carrito en la barra de navegación y la pantalla de resumen del carrito) que los datos han cambiado, para que ellos se vuelvan a renderizar con la información nueva.

Este patrón se llama Flujo de Datos Unidireccional. Los datos fluyen en una sola dirección: desde el estado global hacia los componentes que los muestran (mediante "props" o "hooks" como useSelector o useContext). Las acciones del usuario (como hacer clic en "Añadir al carrito") generan "eventos" o "acciones" que viajan hacia arriba para modificar el estado global. Este ciclo evita que los datos se desincronicen y hace que el comportamiento de la aplicación sea predecible y fácil de depurar, ya que siempre se puede rastrear cómo y por qué cambió el estado.

Tip del Instructor: Para proyectos de escala media como una app de e-commerce, Zustand es una excelente opción por su simplicidad y potencia. Ofrece un store global sin la ceremonia de Redux, pero con capacidades similares. Es el "gestor de almacén" ágil y moderno.

Cómo Funciona en la Práctica: Arquitectura Paso a Paso

Vamos a desglosar el proceso de implementación en pasos concretos. Paso 1: Modelado de Datos. Definimos las interfaces TypeScript para nuestro producto y el ítem del carrito. El producto tendrá propiedades como `id`, `name`, `price`, `description`, `imageUrl` y `stock`. El ítem del carrito (`CartItem`) extenderá del producto y añadirá una propiedad `quantity`. Paso 2: Creación del Store del Carrito. Utilizando Zustand, creamos un hook personalizado (`useCartStore`) que contendrá el estado (un array de `CartItem`) y las acciones para modificarlo: `addToCart`, `removeFromCart`, `updateQuantity`, `clearCart` y un selector computado como `totalItems`.

Paso 3: Implementación del Catálogo. Creamos un componente `ProductList` que, usando `FlatList`, renderiza componentes `ProductCard` por cada producto. Este componente recibirá la lista de productos desde una API (simulada inicialmente con datos mock). La `ProductCard` tendrá un botón "Añadir al carrito" que, al presionarse, llamará a la acción `addToCart` del store. Paso 4: Conexión de los Componentes. El componente `ProductCard` y el componente `CartIcon` en la barra de navegación se suscribirán al store de Zustand. `ProductCard` podría usar un selector para deshabilitar el botón si el producto ya está en el carrito o no hay stock. `CartIcon` se suscribirá al selector `totalItems` para mostrar la insignia con la cantidad.

Paso 5: Pantalla del Carrito. Creamos una pantalla dedicada (`CartScreen`) que liste todos los `CartItem` usando una `FlatList`. Cada fila permitirá ajustar la cantidad (con botones +/-) o eliminar el ítem, llamando a las acciones correspondientes del store. Esta pantalla también mostrará los cálculos de totales, que son selectores derivados del estado (`subtotal`, `tax`, `total`). La persistencia se puede añadir fácilmente con un middleware de Zustand que guarde el estado en `AsyncStorage` en cada cambio.

Código en Acción: Store del Carrito y Componente de Producto

A continuación, un ejemplo funcional y completo de un store de carrito con Zustand y un componente de tarjeta de producto conectado.

Store del Carrito (store/cartStore.ts)


import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

export interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
  stock: number;
}

export interface CartItem extends Product {
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addToCart: (product: Product) => void;
  removeFromCart: (productId: string) => void;
  updateQuantity: (productId: string, newQuantity: number) => void;
  clearCart: () => void;
  totalItems: () => number;
  subtotal: () => number;
}

export const useCartStore = create()(
  persist(
    (set, get) => ({
      items: [],

      addToCart: (product) => {
        const { items } = get();
        const existingItemIndex = items.findIndex(item => item.id === product.id);

        if (existingItemIndex > -1) {
          // Si ya existe, incrementar cantidad si hay stock
          const updatedItems = [...items];
          const currentItem = updatedItems[existingItemIndex];
          if (currentItem.quantity < currentItem.stock) {
            updatedItems[existingItemIndex].quantity += 1;
            set({ items: updatedItems });
          }
        } else {
          // Si no existe, añadirlo con cantidad 1
          if (product.stock > 0) {
            const newItem: CartItem = { ...product, quantity: 1 };
            set({ items: [...items, newItem] });
          }
        }
      },

      removeFromCart: (productId) => {
        set((state) => ({
          items: state.items.filter(item => item.id !== productId)
        }));
      },

      updateQuantity: (productId, newQuantity) => {
        if (newQuantity < 1) {
          get().removeFromCart(productId);
          return;
        }
        set((state) => ({
          items: state.items.map(item =>
            item.id === productId ? { ...item, quantity: Math.min(newQuantity, item.stock) } : item
          )
        }));
      },

      clearCart: () => set({ items: [] }),

      totalItems: () => {
        return get().items.reduce((sum, item) => sum + item.quantity, 0);
      },

      subtotal: () => {
        return get().items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
      },
    }),
    {
      name: 'cart-storage', // clave para AsyncStorage
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);

Componente de Tarjeta de Producto (components/ProductCard.tsx)


import React from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { useCartStore, Product } from '../store/cartStore';

interface ProductCardProps {
  product: Product;
}

const ProductCard: React.FC = ({ product }) => {
  const { items, addToCart } = useCartStore();
  const itemInCart = items.find(item => item.id === product.id);
  const availableStock = itemInCart ? product.stock - itemInCart.quantity : product.stock;
  const isOutOfStock = availableStock <= 0;

  const handleAddToCart = () => {
    if (isOutOfStock) {
      Alert.alert('Stock agotado', `Lo sentimos, no hay más unidades disponibles de ${product.name}.`);
      return;
    }
    addToCart(product);
  };

  return (
    
      
      
        {product.name}
        {product.description}
        
          ${product.price.toFixed(2)}
          
            
              {isOutOfStock ? 'Sin Stock' : (itemInCart ? `Añadir otro (${availableStock})` : 'Añadir al carrito')}
            
          
        
        {itemInCart && (
          En carrito: {itemInCart.quantity} unidad(es)
        )}
      
    
  );
};

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    borderRadius: 12,
    marginVertical: 8,
    marginHorizontal: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
    overflow: 'hidden',
  },
  image: {
    width: '100%',
    height: 180,
  },
  infoContainer: {
    padding: 12,
  },
  name: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 4,
  },
  description: {
    fontSize: 14,
    color: '#666',
    marginBottom: 12,
  },
  footer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  price: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#2E7D32',
  },
  button: {
    backgroundColor: '#3B82F6',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 8,
  },
  buttonDisabled: {
    backgroundColor: '#9CA3AF',
  },
  buttonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 14,
  },
  inCartText: {
    fontSize: 12,
    color: '#3B82F6',
    marginTop: 8,
    fontStyle: 'italic',
  },
});

export default ProductCard;

Componente de Ícono del Carrito (components/CartIcon.tsx)


import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';
import { useCartStore } from '../store/cartStore';

const CartIcon: React.FC = () => {
  const navigation = useNavigation();
  const totalItems = useCartStore((state) => state.totalItems());

  return (
     navigation.navigate('Cart' as never)} style={styles.container}>
      
      {totalItems > 0 && (
        
          {totalItems > 99 ? '99+' : totalItems}
        
      )}
    
  );
};

const styles = StyleSheet.create({
  container: {
    marginRight: 16,
    padding: 4,
  },
  badge: {
    position: 'absolute',
    right: -6,
    top: -4,
    backgroundColor: '#EF4444',
    borderRadius: 10,
    minWidth: 20,
    height: 20,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 4,
  },
  badgeText: {
    color: 'white',
    fontSize: 12,
    fontWeight: 'bold',
  },
});

export default CartIcon;

Errores Comunes y Cómo Evitarlos

1. Mutación Directa del Estado: Modificar directamente el array `state.items` con `push()` o cambiar una propiedad de un objeto dentro del array. Esto rompe la inmutabilidad y puede causar que los componentes no se re-rendericen correctamente. Solución: Siempre crear nuevas referencias. Usar operadores de propagación (`...`) o métodos como `map`, `filter`, y `slice` que devuelven nuevos arrays.

2. Lógica de Negocio Duplicada en Componentes: Poner la validación de stock o el cálculo de totales dentro de los componentes UI (como `CartScreen`). Esto hace el código difícil de mantener y testear. Solución: Centralizar toda la lógica de negocio dentro del store (como se ve en `addToCart` y `subtotal`). Los componentes solo deben despachar acciones y mostrar datos.

3. No Manejar la Persistencia de Forma Asíncrona: Intentar acceder al estado persistido (ej: desde `AsyncStorage`) de manera síncrona al iniciar la app, lo que puede causar que el render inicial muestre un estado vacío o incorrecto. Solución: Usar middlewares como `persist` de Zustand o `redux-persist` que manejan la hidratación del estado automáticamente. Mostrar un splash screen o un indicador de carga mientras se carga el estado persistido.

4. Olvidar la Gestión de Memoria en Listas Grandes: Usar `ScrollView` para renderizar un catálogo con cientos de productos, lo que carga todos los elementos en memoria de una vez y mata el rendimiento. Solución: Usar siempre `FlatList` o `FlashList` para listas largas. Estas componentes renderizan solo los ítems que están en pantalla (ventana visible) y reciclan los elementos al hacer scroll.

5. Sincronización Incorrecta de IDs o Claves: Usar el índice del array como `key` en las listas o tener inconsistencias entre el `id` del producto en el catálogo y el `id` referenciado en el carrito. Solución: Asegurarse de que cada producto tenga un `id` único y estable (preferiblemente del backend). Usar siempre ese `id` como `key` en las `FlatList` y como referencia en las funciones del store. Nunca usar el índice.

Tip de Depuración: Cuando un componente no se actualiza tras una acción del carrito, revisa primero que estás usando el hook del store correctamente (ej: `useCartStore()`) y que no estás mutando el estado accidentalmente. Usa consolas temporales en las funciones del store para verificar que se están llamando.

Checklist de Dominio

  • He implementado un store global (con Zustand, Context o Redux) que gestiona un array de ítems del carrito con sus cantidades.
  • Mi store incluye acciones puras e inmutables para añadir, eliminar, actualizar cantidad y limpiar el carrito.
  • He implementado selectores derivados (como `totalItems` y `subtotal`) que se calculan eficientemente a partir del estado base.
  • El catálogo de productos utiliza `FlatList` para un renderizado eficiente y cada tarjeta de producto está conectada al store para deshabilitar botones o mostrar información contextual (ej: "ya en el carrito").
  • Un ícono de carrito en la barra de navegación se actualiza en tiempo real mostrando la cantidad total de ítems, usando una suscripción al store.
  • La pantalla del carrito lista los productos, permite modificar cantidades, eliminar ítems y muestra un resumen de compra con cálculos actualizados al instante.
  • El estado del carrito persiste entre sesiones de la app usando `AsyncStorage` o un middleware similar.
  • He manejado casos de borde: stock agotado, cantidades que superan el stock, y productos duplicados al añadir al carrito.
Falar no WhatsApp
De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión

Implementación de Catálogo de Productos y Carrito | Cursalo