Integración Final y Flujo de Usuario Completo
En esta etapa culminante, nuestro objetivo es ensamblar todos los módulos independientes que hemos construido—autenticación, catálogo de productos, carrito de compras, checkout y perfil de usuario—en una única aplicación cohesiva. Esto va más allá de simplemente conectar pantallas; se trata de garantizar que el estado de la aplicación (como el usuario logueado, los items en el carrito y las preferencias) persista y sea accesible de manera consistente a lo largo de toda la experiencia del usuario. Utilizaremos un gestor de estado global, como React Context o Redux Toolkit, para crear esta fuente única de verdad.
El flujo de usuario completo que debemos implementar y probar secuencialmente incluye: registro de una nueva cuenta, inicio de sesión, navegación por el catálogo con filtros, adición y remoción de productos del carrito, aplicación de códigos de descuento, proceso de checkout con simulación de pago, visualización del historial de pedidos y actualización del perfil. Cada transición entre estos pasos debe ser fluida, manejando estados de carga y errores potenciales de red de forma elegante, para proporcionar una experiencia profesional.
Además, es crucial configurar la navegación (con React Navigation) para reflejar el estado de autenticación. Usuarios no autenticados deben ser redirigidos a las pantallas de login/registro cuando intenten acceder a áreas protegidas como el carrito o el perfil, mientras que los usuarios autenticados deben poder navegar libremente. Esta capa de lógica de navegación es el esqueleto que sostiene toda la app.
Concepto Clave: Estado Global y Fuente Única de la Verdad
Imagina que estás coordinando una obra de teatro. El estado global es como el guión maestro que todos los actores (componentes) tienen y al que deben referirse. En él está escrito quién está en escena (usuario logueado), qué props hay en ella (productos en el carrito) y cuál es la escena actual (pantalla activa). Si cada actor tuviera su propio guión personal y lo modificara, pronto habría caos: un actor podría pensar que hay una mesa en escena mientras otro la ha removido. La fuente única de la verdad evita esta inconsistencia.
En React Native, sin un estado global, pasaríamos datos (props) desde un componente abuelo, a un padre, a un hijo, y luego a un nieto—un proceso llamado "prop drilling". Esto es engorroso y propenso a errores. Al centralizar el estado en un store (como el de Redux) o en un Context Provider, cualquier componente, sin importar su profundidad en el árbol, puede "suscribirse" a los datos que necesita y despachar acciones para modificarlos. Para nuestra app de e-commerce, el estado global típicamente contiene el objeto del usuario, la lista del carrito, el historial de pedidos y temas como el modo claro/oscuro.
La analogía del carrito de supermercado es perfecta. El carrito físico es tu estado global. Puedes agregar un producto (disparar una acción) desde cualquier pasillo (componente), y el carrito (el store) se actualiza. Cuando vas a la caja (pantalla de checkout), la cajera (componente checkout) no necesita que le pases cada item uno por uno; simplemente lee el contenido completo del carrito (accede al estado global). Si decides dejar un producto (remover una acción), el cambio se refleja inmediatamente en el carrito y, por ende, en lo que la cajera ve.
Cómo Funciona en la Práctica: Configuración del Store y Navegación Condicional
Vamos a implementar esto paso a paso. Primero, configuraremos un store de Redux Toolkit. Crearemos "slices" para las entidades principales: `authSlice` para el usuario y token, `cartSlice` para los items del carrito y `ordersSlice` para el historial. Cada slice define su estado inicial, reducers (funciones que saben cómo actualizar el estado) y acciones. Luego, envolveremos nuestro componente raíz `App` con el `Provider` de Redux, haciendo el store disponible en toda la aplicación.
El segundo paso crucial es la navegación condicional. Usando React Navigation, crearemos dos navegadores principales: un `Stack.Navigator` para las pantallas de autenticación (Login, Registro) y un `Tab.Navigator` o `Drawer.Navigator` para la app principal (Inicio, Carrito, Perfil). En nuestro componente `App`, consultaremos el estado global de autenticación (por ejemplo, `const isLoggedIn = useSelector(state => state.auth.token)`) y renderizaremos condicionalmente uno u otro navegador. Esto crea una experiencia de app nativa donde el login te "lleva dentro" de la aplicación principal.
Finalmente, conectaremos las pantallas de la app principal al store. Por ejemplo, en la pantalla `ProductList`, haremos un `useDispatch` para despachar la acción `addToCart` cuando el usuario presione un botón. En la pantalla `CartScreen`, usaremos `useSelector` para obtener la lista actual del carrito y mapearla para mostrarla. Cualquier cambio despachado desde cualquier pantalla actualizará automáticamente el `CartScreen` gracias a la suscripción de React Redux, manteniendo la UI perfectamente sincronizada con el estado.
Código en Acción: Store de Redux y Componente de Navegación Principal
A continuación, un ejemplo práctico y funcional de la configuración central del estado y la navegación.
1. Store de Redux Toolkit (store.js)
import { configureStore, createSlice } from '@reduxjs/toolkit';
// Slice para el Carrito
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
total: 0,
},
reducers: {
addItem: (state, action) => {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
state.total = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
},
clearCart: (state) => {
state.items = [];
state.total = 0;
},
},
});
// Slice para Autenticación
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: null,
},
reducers: {
setCredentials: (state, action) => {
const { user, accessToken } = action.payload;
state.user = user;
state.token = accessToken;
},
logout: (state) => {
state.user = null;
state.token = null;
},
},
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export const { setCredentials, logout } = authSlice.actions;
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
cart: cartSlice.reducer,
// Aquí se agregarían orders, products, etc.
},
});
2. Navegación Condicional en App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Provider, useSelector } from 'react-redux';
import { store } from './src/store/store';
import LoginScreen from './src/screens/LoginScreen';
import HomeScreen from './src/screens/HomeScreen';
import CartScreen from './src/screens/CartScreen';
import ProfileScreen from './src/screens/ProfileScreen';
import { Ionicons } from '@expo/vector-icons';
const AuthStack = createNativeStackNavigator();
const MainTab = createBottomTabNavigator();
function AuthNavigator() {
return (
{/* */}
);
}
function MainNavigator() {
return (
({
tabBarIcon: ({ color, size }) => {
let iconName;
if (route.name === 'Inicio') iconName = 'home';
else if (route.name === 'Carrito') iconName = 'cart';
else if (route.name === 'Perfil') iconName = 'person';
return ;
},
})}>
);
}
function RootNavigator() {
const token = useSelector(state => state.auth.token);
return token ? : ;
}
export default function App() {
return (
);
}
3. Uso del Store en una Pantalla de Producto (ProductCard.js)
import React from 'react'; import { View, Text, Button, Image, StyleSheet } from 'react-native'; import { useDispatch } from 'react-redux'; import { addItem } from '../store/store'; const ProductCard = ({ product }) => { const dispatch = useDispatch(); const handleAddToCart = () => { // Despachamos la acción para agregar al carrito global dispatch(addItem(product)); // Podríamos mostrar un feedback visual aquí (ej: un toast) alert(`${product.name} agregado al carrito!`); }; return ({product.name} ${product.price.toFixed(2)} {product.description}
); }; const styles = StyleSheet.create({ card: { padding: 15, margin: 10, backgroundColor: '#fff', borderRadius: 8, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4 }, image: { width: '100%', height: 150, resizeMode: 'cover', borderRadius: 5 }, name: { fontSize: 18, fontWeight: 'bold', marginTop: 8 }, price: { fontSize: 16, color: '#2ecc71', marginVertical: 4 }, description: { fontSize: 14, color: '#7f8c8d' }, }); export default ProductCard;
Errores Comunes y Cómo Evitarlos
1. Mutación Directa del Estado en Reducers: En Redux, el estado es inmutable. Un error común es intentar modificar un objeto o array existente directamente (ej: `state.items.push(newItem)`). Esto causa bugs sutiles y rompe la trazabilidad. Solución: Redux Toolkit's `createSlice` usa Immer internamente, permitiendo escribir código que "parece" mutar el estado. Pero si no usas Toolkit, siempre debes retornar un nuevo estado (ej: `return { ...state, items: [...state.items, newItem] }`).
2. Navegación Condicional Mal Implementada: Renderizar el `NavigationContainer` dentro de un componente condicional que se re-renderiza frecuentemente, o no manejar correctamente el estado de hidratación tras un refresh, puede causar pantallas en blanco o loops de navegación. Solución: Mantén el `NavigationContainer` en el nivel más alto posible (como en el ejemplo) y maneja la lógica condicional dentro de un navegador raíz (`RootNavigator`). Usa estados de carga (`isLoading`) para evitar flashes de pantallas de login.
3. No Limpiar el Estado al Cerrar Sesión: Al hacer logout, si solo rediriges al usuario a la pantalla de login pero no restableces el store global, los datos sensibles del usuario anterior (carrito, información personal) persistirán para el próximo usuario que inicie sesión. Solución: Despacha una acción de `logout` que ponga a `null` el `user` y `token`, y también considere resetear slices sensibles como el `cart` usando `clearCart()`.
4. Olvidar Manejar Estados de Carga y Error en Llamadas API: En una app real, las llamadas a tu backend para login, checkout o fetch de productos pueden fallar o demorarse. No mostrar indicadores de carga o mensajes de error confunde al usuario. Solución: Usa estados locales (`useState`) o dentro de tus slices de Redux para manejar `loading`, `error` y `data`. Muestra un `ActivityIndicator` mientras `loading` es true, y un `Alert` o mensaje en pantalla si `error` no es null.
Tip Profesional: Para depurar el estado global de Redux, instala la extensión "React Native Debugger" o usa "Redux DevTools". Te permite viajar en el tiempo entre acciones, inspeccionar el estado completo y entender exactamente qué está sucediendo en tu app, acelerando enormemente el desarrollo y la resolución de bugs.
Checklist de Dominio
Antes de considerar esta práctica completa y funcional, verifica que puedes realizar y comprender cada uno de los siguientes puntos:
- Puedo explicar la diferencia entre estado local (useState) y estado global (Redux/Context) y cuándo usar cada uno.
- He configurado un store de Redux Toolkit con al menos dos slices (ej: auth y cart) y los he integrado en mi aplicación Expo.
- He implementado una navegación condicional que muestra un flujo de autenticación (Login/Registro) si no hay token, y un flujo principal de tabs/drawer si el usuario está autenticado.
- Puedo despachar acciones (ej: addToCart, login) desde cualquier pantalla y ver cómo la UI de otras pantallas conectadas (como el ícono del carrito o la pantalla del carrito) se actualiza instantáneamente.
- He simulado un flujo de usuario completo: registro, login, agregar 3 productos al carrito, proceder al checkout (con una simulación de pago), y ver el pedido confirmado.
- He manejado al menos un estado de carga y un error durante una operación asíncrona (ej: al enviar el formulario de login con credenciales incorrectas).
- Puedo persistir algún dato (como el token de autenticación) usando AsyncStorage o SecureStore, y recuperarlo al reiniciar la app para un login automático.
- He probado la app en ambos entornos, Android e iOS (físico o emulador), y he verificado que el layout y la funcionalidad sean consistentes.