Typescript
arquitectura-de-software
react native
mobile development
Modular Architecture
Cuando empezaste a trabajar con React Native, todo parecía manejable. Un par de pantallas, algunos componentes, tal vez un formulario o dos. Todo vivía felizmente en una carpeta screens, otra carpeta components, y listo. Funcionaba. Pero entonces el proyecto creció. De pronto tenías 20 pantallas, 50 componentes, lógica de autenticación mezclada con lógica de perfil, y cada vez que necesitabas cambiar algo, pasabas 10 minutos buscando dónde habías puesto ese archivo.
En este articulo vamos a hablar de arquitectura modular: qué es, por qué importa, y cómo implementarla en React Native CLI con TypeScript de una forma que realmente tenga sentido.
Imagina que estás organizando tu casa. Puedes tener todo amontonado en una habitación gigante; ropa, libros, platos, herramientas... o puedes tener habitaciones separadas, cocina, dormitorio, estudio, cada una con sus propias cosas y su propia función.
La arquitectura modular es exactamente eso, pero para tu código. En lugar de tener todo mezclado en carpetas genéricas como screens o components, organizas tu aplicación por características o funcionalidades (features). Cada feature es como su propia mini-aplicación dentro de la aplicación grande.
Por ejemplo:
authprofileCada módulo contiene sus propias pantallas, componentes, hooks, y lógica de estado. Es autocontenido y no depende innecesariamente de otros módulos.
1/src 2 /screens 3 LoginScreen.tsx 4 RegisterScreen.tsx 5 ProfileScreen.tsx 6 DashboardScreen.tsx 7 SettingsScreen.tsx 8 // ... 20 pantallas más 9 /components 10 Button.tsx 11 Input.tsx 12 ProfileHeader.tsx 13 DashboardCard.tsx 14 // ... 50 componentes más 15 /hooks 16 useAuth.ts 17 useProfile.ts 18 // ... mezclado todo
Esto funciona cuando eres solo tú y tienes 5 pantallas. Pero cuando el proyecto crece:
1/src 2 /features 3 /auth 4 /screens 5 LoginScreen.tsx 6 RegisterScreen.tsx 7 /components 8 LoginForm.tsx 9 SocialLoginButtons.tsx 10 /hooks 11 useAuth.ts 12 /store 13 authStore.ts 14 /navigation 15 AuthNavigator.tsx 16 index.ts 17 /profile 18 /screens 19 ProfileScreen.tsx 20 /components 21 ProfileHeader.tsx 22 ProfileStats.tsx 23 /hooks 24 useProfile.ts 25 index.ts 26 /dashboard 27 // ... su propia estructura 28 /components 29 // Componentes compartidos (Button, Input, Card) 30 /hooks 31 // Hooks compartidos (useKeyboard, useDebounce) 32 /store 33 // Store global si usas Zustand, Redux, etc. 34 /navigation 35 RootNavigator.tsx 36 /utils 37 /types
Ahora todo relacionado con autenticación vive junto. Si quieres trabajar en el login, sabes exactamente dónde ir. Si otro desarrollador está trabajando en el dashboard, no se cruzan en tu camino.
Vamos a crear un ejemplo práctico. Supongamos que estás construyendo una app de fitness con autenticación, perfil de usuario, y un dashboard de estadísticas.
Cada feature tiene esta estructura básica:
1/features 2 /auth 3 /screens # Pantallas de este módulo 4 /components # Componentes específicos de este módulo 5 /hooks # Hooks específicos de este módulo 6 /store # Estado local del módulo 7 /types # Tipos TypeScript del módulo 8 /utils # Utilidades específicas 9 index.ts # Exportaciones públicas del módulo
El archivo index.ts es crucial: controla qué exporta tu módulo. Piensa en él como la puerta de entrada del módulo.
1// src/features/auth/index.ts 2 3export { LoginScreen, RegisterScreen } from './screens'; 4export { useAuth } from './hooks/useAuth'; 5export { authStore } from './store/authStore'; 6export type { User, AuthState } from './types'; 7 8// Los componentes internos NO se exportan 9// Esto mantiene la encapsulación del módulo
Vamos a construir un módulo de autenticación completo pero simple.
Store con Zustand:
1// src/features/auth/store/authStore.ts 2 3import { create } from 'zustand'; 4import type { User } from '../types'; 5 6interface AuthState { 7 user: User | null; 8 isAuthenticated: boolean; 9 isLoading: boolean; 10 login: (email: string, password: string) => Promise<void>; 11 logout: () => void; 12} 13 14export const useAuthStore = create<AuthState>((set) => ({ 15 user: null, 16 isAuthenticated: false, 17 isLoading: false, 18 19 login: async (email, password) => { 20 set({ isLoading: true }); 21 22 try { 23 // Aquí iría tu llamada a API 24 const response = await fetch('https://example.com/login', { 25 method: 'POST', 26 headers: { 'Content-Type': 'application/json' }, 27 body: JSON.stringify({ email, password }), 28 }); 29 30 const user = await response.json(); 31 32 set({ 33 user, 34 isAuthenticated: true, 35 isLoading: false 36 }); 37 } catch (error) { 38 set({ isLoading: false }); 39 throw error; 40 } 41 }, 42 43 logout: () => { 44 set({ 45 user: null, 46 isAuthenticated: false 47 }); 48 }, 49}));
Hook personalizado:
1// src/features/auth/hooks/useAuth.ts 2 3import { useAuthStore } from '../store/authStore'; 4 5/** 6 * Hook que encapsula la lógica de autenticación. 7 * Otros módulos pueden usar este hook sin conocer 8 * los detalles internos del store. 9 */ 10export const useAuth = () => { 11 const { user, isAuthenticated, isLoading, login, logout } = useAuthStore(); 12 13 // Podrías agregar lógica adicional aquí 14 const isAdmin = user?.role === 'admin'; 15 16 return { 17 user, 18 isAuthenticated, 19 isLoading, 20 isAdmin, 21 login, 22 logout, 23 }; 24};
Pantalla de Login:
1// src/features/auth/screens/LoginScreen.tsx 2 3import React, { useState } from 'react'; 4import { View, StyleSheet } from 'react-native'; 5import { useAuth } from '../hooks/useAuth'; 6import { LoginForm } from '../components/LoginForm'; 7 8export const LoginScreen = () => { 9 const { login, isLoading } = useAuth(); 10 const [error, setError] = useState<string | null>(null); 11 12 const handleLogin = async (email: string, password: string) => { 13 try { 14 setError(null); 15 await login(email, password); 16 // La navegación se maneja automáticamente cuando isAuthenticated cambia 17 } catch (err) { 18 setError('Credenciales inválidas'); 19 } 20 }; 21 22 return ( 23 <View style={styles.container}> 24 <LoginForm 25 onSubmit={handleLogin} 26 isLoading={isLoading} 27 error={error} 28 /> 29 </View> 30 ); 31}; 32 33const styles = StyleSheet.create({ 34 container: { 35 flex: 1, 36 justifyContent: 'center', 37 padding: 20, 38 }, 39});
Aquí es donde la cosa se pone interesante. ¿Cómo hace el módulo profile para acceder a la información del usuario que está en auth?
La regla de oro: usa las exportaciones públicas
1// src/features/profile/screens/ProfileScreen.tsx 2 3import React from 'react'; 4import { View, Text, StyleSheet } from 'react-native'; 5// ✅ Importas desde el index.ts del módulo auth 6import { useAuth } from '../../auth'; 7 8export const ProfileScreen = () => { 9 // Usas el hook público que exporta el módulo de auth 10 const { user } = useAuth(); 11 12 if (!user) { 13 return <Text>Cargando...</Text>; 14 } 15 16 return ( 17 <View style={styles.container}> 18 <Text style={styles.name}>{user.name}</Text> 19 <Text style={styles.email}>{user.email}</Text> 20 </View> 21 ); 22}; 23 24const styles = StyleSheet.create({ 25 container: { 26 flex: 1, 27 padding: 20, 28 }, 29 name: { 30 fontSize: 24, 31 fontWeight: 'bold', 32 marginBottom: 8, 33 }, 34 email: { 35 fontSize: 16, 36 color: '#666', 37 }, 38});
Lo que NO debes hacer:
1// ❌ NUNCA importes directamente desde archivos internos 2import { useAuthStore } from '../../auth/store/authStore'; 3 4// ✅ SIEMPRE usa las exportaciones públicas 5import { useAuth } from '../../auth';
¿Por qué? Porque si mañana decides cambiar Zustand por Redux en el módulo auth, solo necesitas actualizar el módulo auth y su hook useAuth. Todos los demás módulos siguen funcionando sin cambios.
Esta es una pregunta que surge constantemente ¿dónde pongo mis componentes? Piensa en LEGO. Tienes piezas específicas (como la cabeza de un Stormtrooper que solo usas en sets de Star Wars) y piezas genéricas (como los bloques básicos que usas en cualquier construcción).
Componentes del módulo (específicos):
1// src/features/auth/components/LoginForm.tsx 2// Solo se usa en el módulo de auth
Componentes compartidos (genéricos):
1// src/components/Button.tsx 2// Se usa en múltiples módulos 3 4import React from 'react'; 5import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; 6 7interface ButtonProps { 8 title: string; 9 onPress: () => void; 10 isLoading?: boolean; 11 variant?: 'primary' | 'secondary'; 12} 13 14export const Button: React.FC<ButtonProps> = ({ 15 title, 16 onPress, 17 isLoading, 18 variant = 'primary' 19}) => { 20 return ( 21 <TouchableOpacity 22 style={[styles.button, styles[variant]]} 23 onPress={onPress} 24 disabled={isLoading} 25 > 26 {isLoading ? ( 27 <ActivityIndicator color="white" /> 28 ) : ( 29 <Text style={styles.text}>{title}</Text> 30 )} 31 </TouchableOpacity> 32 ); 33}; 34 35const styles = StyleSheet.create({ 36 button: { 37 padding: 16, 38 borderRadius: 8, 39 alignItems: 'center', 40 }, 41 primary: { 42 backgroundColor: '#007AFF', 43 }, 44 secondary: { 45 backgroundColor: '#8E8E93', 46 }, 47 text: { 48 color: 'white', 49 fontSize: 16, 50 fontWeight: '600', 51 }, 52});
1// src/features/auth/navigation/AuthNavigator.tsx 2 3import React from 'react'; 4import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5import { LoginScreen } from '../screens/LoginScreen'; 6import { RegisterScreen } from '../screens/RegisterScreen'; 7 8const Stack = createNativeStackNavigator(); 9 10export const AuthNavigator = () => { 11 return ( 12 <Stack.Navigator screenOptions={{ headerShown: false }}> 13 <Stack.Screen name="Login" component={LoginScreen} /> 14 <Stack.Screen name="Register" component={RegisterScreen} /> 15 </Stack.Navigator> 16 ); 17};
1// src/navigation/RootNavigator.tsx 2 3import React from 'react'; 4import { NavigationContainer } from '@react-navigation/native'; 5import { createNativeStackNavigator } from '@react-navigation/native-stack'; 6import { useAuth } from '../features/auth'; 7import { AuthNavigator } from '../features/auth/navigation/AuthNavigator'; 8import { MainTabNavigator } from './MainTabNavigator'; 9 10const Stack = createNativeStackNavigator(); 11 12export const RootNavigator = () => { 13 const { isAuthenticated } = useAuth(); 14 15 return ( 16 <NavigationContainer> 17 <Stack.Navigator screenOptions={{ headerShown: false }}> 18 {isAuthenticated ? ( 19 <Stack.Screen name="Main" component={MainTabNavigator} /> 20 ) : ( 21 <Stack.Screen name="Auth" component={AuthNavigator} /> 22 )} 23 </Stack.Navigator> 24 </NavigationContainer> 25 ); 26};
Aquí hay una pregunta práctica: ¿cuándo usar un store global y cuándo un store por módulo?
Store de módulo: Cuando el estado solo importa dentro de ese módulo. Por ejemplo: El estado de un formulario dentro de una feature, datos temporales de una pantalla o estado de UI específico del módulo.
Store global: Cuando múltiples módulos necesitan acceder a la misma información. Por ejemplo: Datos del usuario autenticado (varios módulos lo necesitan), tema de la app (dark/light mode), configuración general o cache de datos compartidos.
Ejemplo de store global simple:
1// src/store/appStore.ts 2 3import { create } from 'zustand'; 4 5interface AppState { 6 theme: 'light' | 'dark'; 7 setTheme: (theme: 'light' | 'dark') => void; 8} 9 10export const useAppStore = create<AppState>((set) => ({ 11 theme: 'light', 12 setTheme: (theme) => set({ theme }), 13}));
Una ventaja enorme de esta arquitectura es que puedes tipar todo de forma muy clara:
1// src/features/auth/types/index.ts 2 3export interface User { 4 id: string; 5 name: string; 6 email: string; 7 role: 'user' | 'admin'; 8} 9 10export interface AuthState { 11 user: User | null; 12 isAuthenticated: boolean; 13 isLoading: boolean; 14} 15 16export interface LoginCredentials { 17 email: string; 18 password: string; 19}
Y luego exportar solo lo necesario:
1// src/features/auth/index.ts 2 3export type { User, AuthState } from './types'; 4// No exportas LoginCredentials porque es interno del módulo
Esto te da encapsulación: otros módulos solo ven lo que necesitan ver.
Después de adoptar esta arquitectura, notarás cambios tangibles:
1. Onboarding más rápido
Cuando entra alguien nuevo al equipo, le dices: "Vas a trabajar en el módulo de pagos, todo está en /features/payments". No tiene que buscar archivos en 10 carpetas diferentes.
2. Menos conflictos en Git
Si tú trabajas en auth y tu compañero en dashboard, rara vez tocan los mismos archivos. Los merge conflicts bajan dramáticamente.
3. Refactors más seguros
Puedes reescribir completamente el módulo de profile sin tocar nada más. Si las exportaciones públicas siguen siendo las mismas, el resto de la app ni se entera.
4. Testeo más fácil Cada módulo se puede testear de forma aislada. No necesitas mockear media aplicación para probar una funcionalidad.
5. Claridad mental Cuando abres tu editor, ves una estructura que tiene sentido. No hay ese momento de "¿dónde demonios puse esto?" que te roba 15 minutos de tu día.
La arquitectura modular no es un concepto nuevo ni revolucionario. Es simplemente una forma de organizar tu código que respeta cómo funciona tu cerebro: agrupando cosas relacionadas y separando cosas diferentes. Al principio puede parecer más trabajo. "¿Por qué crear tantas carpetas?" Pero cuando tu app crece, cuando trabajas en equipo, cuando tienes que volver a un proyecto después de 6 meses, esta inversión inicial se paga sola mil veces.