Introducción: La Importancia de la Arquitectura en un Proyecto Real
Antes de escribir una sola línea de código para nuestra aplicación de comercio electrónico, debemos dedicar tiempo a diseñar su arquitectura. Este paso es el equivalente a un arquitecto creando los planos detallados de un edificio antes de que comience la construcción. Saltarse esta fase es uno de los errores más costosos que un desarrollador puede cometer, especialmente en proyectos de escala intermedia a avanzada como el que estamos por emprender. Una arquitectura bien pensada no solo define cómo se organiza el código, sino que establece las reglas del juego para el crecimiento, el mantenimiento y la colaboración del equipo.
En este proyecto integrador, no estamos construyendo un simple contador o una lista de tareas. Estamos creando un sistema complejo que involucra gestión de estado global (carrito, usuario, productos), navegación entre múltiples pantallas (catálogo, detalle de producto, checkout), persistencia de datos local, llamadas a APIs externas y una interfaz de usuario coherente y responsive. Sin un diseño arquitectónico claro, este código se convertirá rápidamente en un "espagueti" imposible de descifrar, donde un cambio en la pantalla del carrito rompe inesperadamente la pantalla de perfil del usuario.
El objetivo de esta lección es pasar de una idea abstracta ("una app de e-commerce") a un plan de acción concreto y ejecutable. Definiremos la estructura de carpetas, seleccionaremos las librerías clave, modelaremos los datos y trazaremos el flujo de navegación. Al final, tendrás un blueprint o plano técnico que guiará cada una de las siguientes lecciones del módulo, asegurando que cada pieza que construyamos encaje perfectamente con las demás.
Concepto Clave: Arquitectura de Componentes y Separación de Responsabilidades
La arquitectura de software, en el contexto de React Native, se refiere a la organización estructural intencional de tu código, componentes, datos y lógica de negocio. Su propósito principal es manejar la complejidad mediante la división del sistema en partes más pequeñas y manejables, cada una con una responsabilidad única y clara. Piensa en una fábrica de automóviles: no hay una sola estación que construya el coche entero. Hay una línea de ensamblaje con estaciones especializadas: una para el chasis, otra para el motor, otra para la pintura y otra para los interiores. Cada estación sabe exactamente qué hacer, recibe los componentes necesarios y entrega su parte terminada para el siguiente paso.
En nuestra app, aplicamos este principio mediante la separación de responsabilidades. No queremos un componente "God" que lo sepa y lo haga todo. En su lugar, separamos: los componentes de presentación (UI pura, cómo se ve), la lógica de negocio y estado (qué datos se muestran y cómo cambian), la capa de servicios (comunicación con APIs y backend) y la capa de utilidades (funciones helper, constantes). Esta separación hace que el código sea más fácil de probar, depurar y, sobre todo, de modificar. Si mañana cambia el diseño de un botón, solo tocas el componente de presentación, sin riesgo de afectar la lógica que calcula el total del carrito.
Tip del Instructor: Una buena regla general es preguntarte: "¿Este componente o módulo hace más de una cosa?" Si la respuesta es sí, es probable que sea un candidato para ser dividido. Un componente debería tener un solo motivo para cambiar.
Cómo Funciona en la Práctica: Definición de la Estructura del Proyecto
Vamos a traducir los conceptos teóricos a una estructura de carpetas concreta dentro de nuestro proyecto de Expo. Esta estructura será el esqueleto sobre el que construiremos todo. Creamos una raíz de proyecto clara y luego subdividimos en directorios con propósitos específicos. No uses una estructura plana donde todos los archivos estén en una sola carpeta; eso es una receta para el desastre en proyectos de esta envergadura.
Paso a paso, iniciaremos un nuevo proyecto de Expo y luego crearemos manualmente las siguientes carpetas dentro de la raíz del proyecto. Esta organización anticipa el crecimiento de la app y agrupa lógicamente los archivos relacionados. Por ejemplo, todos los componentes reutilizables (botones, tarjetas, inputs) irán en `components/ui`, mientras que los componentes específicos de una pantalla (como la lista de productos) irán en `components/screens`. La carpeta `store` albergará toda la configuración de nuestro estado global (usaremos Zustand, una opción ligera y poderosa), y `services` contendrá los módulos para interactuar con APIs.
Esta estructura no es arbitraria; sigue convenciones ampliamente adoptadas en la comunidad React/React Native y facilita la incorporación de nuevos desarrolladores al proyecto. Cualquier persona que abra el código podrá, en cuestión de segundos, entender dónde buscar la lógica del carrito, los estilos globales o los servicios de autenticación. Es un mapa de navegación para tu código fuente.
Creando la Estructura Base
Primero, crea un nuevo proyecto de Expo. Luego, en la raíz, crea las siguientes carpetas. Puedes hacerlo desde tu terminal o desde el explorador de archivos de tu IDE.
# Navega a tu directorio de proyectos y crea la app con Expo
expo init EcommerceApp --template blank-typescript
cd EcommerceApp
# Crea la estructura de directorios esencial
mkdir -p app/components/ui
mkdir -p app/components/screens
mkdir -p app/navigation
mkdir -p app/store
mkdir -p app/services
mkdir -p app/utils
mkdir -p app/constants
mkdir -p app/assets/images
mkdir -p app/assets/icons
mkdir -p app/hooks
mkdir -p app/types
Código en Acción: Modelado de Tipos y Estado Global Inicial
Con la estructura lista, comenzamos a definir los cimientos de datos. En TypeScript, esto significa crear nuestros tipos e interfaces. Definir los tipos primero es como crear un contrato: establece exactamente qué forma deben tener nuestros datos, lo que previene una enorme cantidad de errores en tiempo de desarrollo. Luego, implementamos un store global usando Zustand. Este store será la "fuente única de la verdad" para los datos que necesitan ser accesibles desde múltiples pantallas, como el carrito de compras y la información del usuario.
Vamos a crear dos archivos fundamentales. Primero, `app/types/index.ts`, donde centralizaremos las definiciones de tipos para Producto, Item del Carrito, Usuario, etc. Segundo, `app/store/useCartStore.ts`, que será nuestro store dedicado para gestionar el estado del carrito. Zustand simplifica enormemente la gestión de estado global comparado con Redux, eliminando gran parte del boilerplate mientras mantiene un patrón claro y fácil de testear.
El siguiente código muestra una implementación inicial y funcional. Nota cómo el store (`useCartStore`) define un estado (el array `items` y el `total`) y acciones para manipularlo (`addToCart`, `removeFromCart`, `clearCart`). Estas acciones son las únicas formas de modificar el estado, lo que hace el flujo de datos predecible. También incluye un persist middleware de Zustand, que automáticamente guardará el carrito en el almacenamiento local del dispositivo, para que no se pierda al cerrar la app.
// Archivo: app/types/index.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
imageUrl: string;
category: string;
stock: number;
}
export interface CartItem {
product: Product;
quantity: number;
}
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
export interface AppState {
cart: {
items: CartItem[];
total: number;
};
user: User | null;
}
// Archivo: app/store/useCartStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CartItem, Product } from '../types';
interface CartStore {
items: CartItem[];
total: number;
addToCart: (product: Product) => void;
removeFromCart: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
calculateTotal: () => number;
}
export const useCartStore = create()(
persist(
(set, get) => ({
items: [],
total: 0,
addToCart: (product: Product) => {
const { items } = get();
const existingItemIndex = items.findIndex(item => item.product.id === product.id);
if (existingItemIndex > -1) {
// Si el producto ya está en el carrito, incrementa la cantidad
const updatedItems = [...items];
updatedItems[existingItemIndex].quantity += 1;
set({ items: updatedItems });
} else {
// Si es un producto nuevo, lo añade al carrito
const newItem: CartItem = { product, quantity: 1 };
set({ items: [...items, newItem] });
}
// Recalcula el total después de cualquier cambio
set({ total: get().calculateTotal() });
},
removeFromCart: (productId: string) => {
const { items } = get();
const filteredItems = items.filter(item => item.product.id !== productId);
set({ items: filteredItems });
set({ total: get().calculateTotal() });
},
updateQuantity: (productId: string, quantity: number) => {
if (quantity < 1) {
get().removeFromCart(productId);
return;
}
const { items } = get();
const updatedItems = items.map(item =>
item.product.id === productId ? { ...item, quantity } : item
);
set({ items: updatedItems });
set({ total: get().calculateTotal() });
},
clearCart: () => {
set({ items: [], total: 0 });
},
calculateTotal: () => {
const { items } = get();
return items.reduce((sum, item) => sum + (item.product.price * item.quantity), 0);
},
}),
{
name: 'cart-storage', // clave única para el almacenamiento
storage: createJSONStorage(() => AsyncStorage), // usa AsyncStorage en React Native
}
)
);
Planificación del Flujo de Navegación con React Navigation
Una app de comercio electrónico tiene un flujo de usuario claro: explorar productos, ver detalles, añadir al carrito, revisar el carrito y proceder al checkout. Nuestra arquitectura de navegación debe reflejar y facilitar este flujo. Utilizaremos React Navigation, el estándar de facto en React Native, para implementar un sistema de navegación robusto que combine un Tab Navigator para las secciones principales (Inicio, Buscar, Carrito, Perfil) y un Stack Navigator anidado dentro de cada pestaña (o globalmente) para manejar las pantallas de detalle y proceso.
Planificamos las pantallas clave: `HomeScreen`, `ProductDetailScreen`, `CartScreen`, `CheckoutScreen`, `ProfileScreen`, `SearchScreen`. La navegación por pestañas es perfecta para las secciones de primer nivel que el usuario necesita frecuentemente. Sin embargo, cuando un usuario toca un producto en la `HomeScreen`, no queremos que navegue a una pestaña diferente; queremos "apilar" la pantalla de detalles sobre la actual, con la posibilidad de volver atrás. Esa es la función del Stack Navigator.
Configuraremos nuestro archivo de navegación principal (`app/navigation/index.tsx` o `AppNavigator.tsx`) para definir esta estructura. Esta separación clara entre la configuración de navegación y el contenido de las pantallas es otro ejemplo de separación de responsabilidades. El navegador no sabe qué hace cada pantalla, solo sabe cómo presentarlas y transicionar entre ellas.
Tip de Navegación: Para una experiencia de usuario óptima, considera usar un Stack Navigator independiente para el flujo de Checkout (Dirección, Pago, Confirmación). Esto aísla ese flujo crítico y permite una gestión de navegación más limpia (por ejemplo, evitar que el usuario vuelva atrás a pantallas de pago después de completar una orden).
Errores Comunes y Cómo Evitarlos
Al diseñar la arquitectura, es fácil caer en trampas que complicarán el desarrollo más adelante. Aquí están los errores más frecuentes y cómo puedes esquivarlos desde el principio.
1. Estado Global Excesivo (Over-engineering): No todo necesita estar en un store global. El estado de un campo de texto en un formulario, o si un modal está visible, suele ser estado local del componente. Usa estado global solo para datos que verdaderamente sean compartidos por múltiples componentes en diferentes ramas del árbol de componentes (como el carrito o el usuario autenticado). Usar `useState` o `useReducer` a nivel de componente es suficiente y más simple para el estado local.
2. Estructura de Carpetas Plana o Desorganizada: El caos en las carpetas lleva al caos en el código. Evita tener docenas de componentes en una sola carpeta `components`. Sigue una convención como la presentada en esta lección (separar UI, screens, features) o adopta una como Feature-Based Folders (agrupar todo relacionado con el "carrito" en una carpeta `features/cart`). La clave es la consistencia.
3. Acoplamiento Excesivo entre Componentes y Lógica: Un componente que hace llamadas a la API, maneja su propio estado, realiza cálculos complejos y renderiza una UI elaborada es un componente acoplado y difícil de probar. Aplica el principio de Single Responsibility. Extrae la lógica de datos en hooks personalizados (por ejemplo, `useProducts`) y la lógica de presentación en componentes puros que solo reciben props.
4. No Planificar la Gestión de Errores y Estados de Carga: La arquitectura debe incluir desde el día uno un plan para los estados de "cargando", "error" y "datos vacíos". No asumas que las llamadas a la API siempre tendrán éxito. Diseña componentes de carga y error reutilizables (``, ``) e intégralos en tu estrategia de fetching de datos (por ejemplo, con React Query o manejo manual de estados en tus hooks).
5. Ignorar la Escalabilidad del Estilo: Usar estilos en línea o StyleSheet.create en cada componente se vuelve ingobernable. Define un sistema de diseño desde el inicio: colores, tipografías, espaciados y bordes en `app/constants/theme.ts`. Crea componentes de UI base (``, ``, ``) que consuman este tema. Esto garantiza consistencia visual y permite cambios de diseño en un solo lugar.
Checklist de Dominio
Antes de proceder a la siguiente lección, asegúrate de poder verificar los siguientes puntos. Esto confirma que has comprendido y puedes aplicar los conceptos clave de la planificación arquitectónica.
- He creado una estructura de carpetas organizada y lógica en mi proyecto de Expo, separando componentes, navegación, estado, servicios y utilidades.
- He definido interfaces TypeScript para las entidades principales de mi app (Producto, Usuario, Item del Carrito).
- He configurado al menos un store global (por ejemplo, para el carrito) usando Zustand, incluyendo acciones básicas (añadir, remover) y persistencia.
- He planificado y esquematizado (en papel o en un diagrama) el flujo de navegación principal de mi app, identificando dónde usaré Stack y Tab Navigators.
- He identificado y listado las librerías clave que usaré (React Navigation, Zustand, AsyncStorage, una para las llamadas a API como Axios o React Query).
- He creado un archivo de constantes o tema para centralizar colores, fuentes y espaciados básicos de mi diseño.
- Comprendo la diferencia entre estado global y estado local, y puedo dar un ejemplo de cuándo usar cada uno en el contexto de la app de e-commerce.
- Sé qué componentes serán puramente de presentación (UI) y cuáles contendrán lógica de negocio o estado, y tengo un plan para separarlos.
Conclusión y Próximos Pasos
La inversión de tiempo en diseñar la arquitectura y planificar el proyecto es la que mayor retorno ofrece en el ciclo de desarrollo. Has pasado de una idea vaga a tener un plano técnico detallado: una estructura de carpetas, tipos de datos definidos, un store global funcional para el carrito y un plan de navegación. Este plano te permitirá avanzar en las siguientes lecciones con confianza, sabiendo exactamente dónde colocar cada nuevo archivo y cómo se integrará con el sistema existente.
En la próxima lección, comenzaremos la construcción activa. Partiremos de esta arquitectura para implementar la capa de servicios y APIs, conectando nuestra app a una fuente de datos real o simulada. Luego, construiremos los componentes de UI base y las primeras pantallas, integrando todo con el store de estado y la navegación que hemos planificado aquí. Recuerda, una arquitectura sólida no es una camisa de fuerza; es un andamio que soporta el crecimiento seguro y ordenado de tu aplicación.
Te animo a que, antes de continuar, tomes el código de ejemplo de esta lección y lo adaptes a tu propio proyecto. Experimenta añadiendo un nuevo campo al tipo `Product`, o crea un store simple para el usuario. Familiarízate con la estructura. Este dominio fundamental es lo que separa a un desarrollador que solo sigue tutoriales de uno que puede construir aplicaciones robustas y mantenibles desde cero.