📱 Application Architecture
Index
- Estructura del Proyecto
- Patrones de Arquitectura
- Flujo de Datos
- Convenciones de Código
- Capas de la Aplicación
- Patrones de Diseño Utilizados
- Manejo de Errores
- Optimización y Performance
Project Structure
proyecto/
├── app/ # Rutas y pantallas (Expo Router)
│ ├── (tabs)/ # Navegación con pestañas
│ │ ├── (home)/ # Tab Home con stack interno
│ │ │ ├── _layout.tsx # Layout del stack
│ │ │ ├── index.tsx # Pantalla principal
│ │ │ └── item-detail.tsx # Detalle de item
│ │ ├── explore/ # Tab Explorar
│ │ ├── favorites/ # Tab Favoritos
│ │ ├── activity/ # Tab Actividad
│ │ ├── messages/ # Tab Mensajes
│ │ ├── profile/ # Tab Perfil
│ │ ├── settings/ # Tab Configuración
│ │ └── _layout.tsx # Configuración de tabs
│ ├── _layout.tsx # Layout raíz
│ ├── notifications.tsx # Modal de notificaciones
│ └── onboarding.tsx # Pantalla de bienvenida
│
├── components/ # Componentes reutilizables
│ ├── Avatar.tsx # Componente de avatar
│ ├── Button.tsx # Botón con variantes
│ ├── Card.tsx # Tarjeta de contenido
│ ├── CategoryChip.tsx # Chip de categoría
│ ├── EmptyState.tsx # Empty state
│ ├── FilterModal.tsx # Modal de filtros
│ ├── Input.tsx # Campo de entrada
│ ├── SearchBar.tsx # Barra de búsqueda
│ ├── SettingsRow.tsx # Fila de configuración
│ ├── SkeletonLoader.tsx # Loader de esqueleto
│ ├── StatCard.tsx # Tarjeta de estadísticas
│ └── Toast.tsx # Notificaciones toast
│
├── contexts/ # Estado global
│ ├── AppContext.tsx # Contexto principal de la app
│ ├── ThemeContext.tsx # Tema (dark/light/system)
│ ├── I18nContext.tsx # Internacionalización
│ └── ToastContext.tsx # Sistema de toasts
│
├── constants/ # Constantes y configuración
│ └── colors.ts # Colores, espaciado, tipografía
│
├── mocks/ # Datos de prueba
│ └── data.ts # Datos mock para desarrollo
│
├── types/ # Definiciones TypeScript
│ └── index.ts # Tipos e interfaces globales
│
├── utils/ # Utilidades y helpers
│ └── helpers.ts # Funciones auxiliares
│
├── docs/ # Documentación
│ ├── INDEX.md # Índice de documentación
│ ├── ARCHITECTURE.md # Este archivo
│ ├── COMPONENTS.md # Guía de componentes
│ ├── STATE_MANAGEMENT.md # Manejo de estado
│ ├── NAVIGATION.md # Sistema de navegación
│ ├── STYLING.md # Sistema de diseño
│ ├── ADDING_FEATURES.md # Agregar funcionalidades
│ ├── AUTHENTICATION.md # Sistema de usuarios
│ ├── DATABASE.md # Base de datos
│ └── REVENUECAT.md # Compras in-app
│
└── hooks/ # Custom hooks (crear si se necesita)
└── useDebounce.ts # Ejemplo de hook
Architecture Patterns
1. Feature-Based Organization
Organizamos el código por funcionalidad, no por tipo de archivo:
✅ Correcto (por feature):
app/(tabs)/
├── messages/
│ ├── _layout.tsx
│ ├── index.tsx # Lista de chats
│ └── [chatId].tsx # Individual chat
❌ Incorrecto (por tipo):
screens/
├── MessageList.tsx
├── Chat.tsx
components/
├── MessageItem.tsx
2. Separación de Responsabilidades
┌─────────────────────────────────────────────────────────────┐
│ UI Layer (Screens) │
│ • Renderiza componentes │
│ • Maneja eventos de usuario │
│ • Consume hooks y contextos │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Logic Layer (Contexts/Hooks) │
│ • Maneja estado global │
│ • Coordina operaciones │
│ • Implementa lógica de negocio │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Data Layer (React Query) │
│ • Fetch de datos │
│ • Caché y sincronización │
│ • Mutaciones │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Persistence Layer (AsyncStorage) │
│ • Almacenamiento local │
│ • Preferencias de usuario │
│ • Cache offline │
└─────────────────────────────────────────────────────────────┘
3. Composición sobre Herencia
Usamos composición de componentes en lugar de herencia:
// ✅ Composición
function ProductCard({ product, actions }: ProductCardProps) {
return (
<Card>
<Card.Image source={product.image} />
<Card.Content>
<Card.Title>{product.name}</Card.Title>
<Card.Description>{product.description}</Card.Description>
</Card.Content>
<Card.Actions>{actions}</Card.Actions>
</Card>
);
}
// ❌ Props excesivas
function ProductCard({
title,
description,
image,
showActions,
actionType,
onAction1,
onAction2,
// ... muchas más props
}) { }
Data Flow
Unidirectional Data Flow
┌────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Acción │────▶│ Context │────▶│ Componentes │ │
│ │ Usuario │ │ / Query │ │ (UI Update) │ │
│ └─────────┘ └──────────┘ └───────────────┘ │
│ ▲ │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ (Eventos) │
│ │
└────────────────────────────────────────────────────────────────┘
Estado y Datos
| Tipo de Estado | Solución | Ejemplo |
|---|---|---|
| Estado de UI local | useState | Modal abierto/cerrado, input value |
| Estado de servidor | React Query | Lista de productos, datos de usuario |
| Estado global de app | Context + createContextHook | Usuario actual, tema, idioma |
| Persistencia local | AsyncStorage | Preferencias, cache offline |
Ejemplo de Flujo Completo
// 1. Usuario presiona "Agregar a favoritos"
<TouchableOpacity onPress={() => toggleFavorite(item.id)}>
// 2. Context procesa la acción
const toggleFavorite = useCallback((itemId: string) => {
const newFavorites = favorites.includes(itemId)
? favorites.filter(id => id !== itemId)
: [...favorites, itemId];
setFavorites(newFavorites);
saveMutation.mutate(newFavorites); // Persiste
}, [favorites, saveMutation]);
// 3. UI se actualiza automáticamente
const isFavorite = favorites.includes(item.id);
<Heart fill={isFavorite ? 'red' : 'transparent'} />
Code Conventions
Nombres de Archivos
| Tipo | Convención | Ejemplo |
|---|---|---|
| Componentes | PascalCase | Button.tsx, ProductCard.tsx |
| Hooks | camelCase con "use" | useDebounce.ts, useAuth.ts |
| Contextos | PascalCase con "Context" | AppContext.tsx, ThemeContext.tsx |
| Utilidades | camelCase | helpers.ts, formatters.ts |
| Tipos | PascalCase | index.ts (dentro de types/) |
| Constantes | camelCase | colors.ts, config.ts |
Estructura de Componentes
// 1. Imports (ordenados: react, react-native, expo, third-party, local)
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { useRouter } from 'expo-router';
import { Heart } from 'lucide-react-native';
import { useTheme } from '@/contexts/ThemeContext';
import { Colors, Spacing } from '@/constants/colors';
// 2. Interface de Props
interface MyComponentProps {
title: string;
subtitle?: string;
onPress?: () => void;
style?: ViewStyle;
testID?: string;
}
// 3. Componente (function declaration, no arrow)
export function MyComponent({
title,
subtitle,
onPress,
style,
testID
}: MyComponentProps) {
// 4. Hooks al inicio
const { colors } = useTheme();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
// 5. Callbacks memoizados
const handlePress = useCallback(() => {
onPress?.();
}, [onPress]);
// 6. Render
return (
<View style={[styles.container, { backgroundColor: colors.surface }, style]} testID={testID}>
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
{subtitle && <Text style={[styles.subtitle, { color: colors.textSecondary }]}>{subtitle}</Text>}
</View>
);
}
// 7. Estilos al final
const styles = StyleSheet.create({
container: {
padding: Spacing.md,
},
title: {
fontSize: 16,
fontWeight: '600' as const,
},
subtitle: {
fontSize: 14,
marginTop: Spacing.xs,
},
});
Convenciones de TypeScript
// ✅ Usar interfaces para props de componentes
interface ButtonProps {
title: string;
onPress: () => void;
}
// ✅ Usar type para uniones y alias
type ButtonVariant = 'primary' | 'secondary' | 'outline';
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
// ✅ Explicitar tipos genéricos en useState
const [items, setItems] = useState<Item[]>([]);
const [selected, setSelected] = useState<string | null>(null);
// ✅ Usar as const para valores literales en estilos
const fontWeight = '600' as const;
// ❌ Evitar any
const data: any = response; // MAL
const data: unknown = response; // MEJOR si no conoces el tipo
Application Layers
Capa de Presentación (app/, components/)
Responsabilidades:
- Renderizar UI
- Manejar interacciones de usuario
- Mostrar estados (loading, error, empty)
- Animaciones y transiciones
// Pantalla típica
export default function ProductListScreen() {
const { products, isLoading, error } = useProducts();
const { colors } = useTheme();
if (isLoading) return <SkeletonLoader />;
if (error) return <ErrorState onRetry={refetch} />;
if (products.length === 0) return <EmptyState />;
return (
<FlatList
data={products}
renderItem={({ item }) => <ProductCard product={item} />}
/>
);
}
Capa de Lógica (contexts/, hooks/)
Responsabilidades:
- Estado global de la aplicación
- Lógica de negocio
- Coordinación entre servicios
- Transformación de datos
// Contexto típico
export const [ProductsProvider, useProducts] = createContextHook(() => {
const queryClient = useQueryClient();
const productsQuery = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
});
const addProductMutation = useMutation({
mutationFn: createProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
// Lógica de filtrado
const getFilteredProducts = useCallback((filters: Filters) => {
return productsQuery.data?.filter(product => {
if (filters.category && product.category !== filters.category) return false;
if (filters.minPrice && product.price < filters.minPrice) return false;
return true;
}) || [];
}, [productsQuery.data]);
return {
products: productsQuery.data || [],
isLoading: productsQuery.isLoading,
addProduct: addProductMutation.mutateAsync,
getFilteredProducts,
};
});
Capa de Datos (lib/, utils/)
Responsabilidades:
- Llamadas a APIs
- Transformación de respuestas
- Manejo de errores de red
- Cache y persistencia
// lib/api/products.ts
const API_URL = process.env.EXPO_PUBLIC_API_URL;
export async function fetchProducts(): Promise<Product[]> {
const response = await fetch(`${API_URL}/products`);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
return data.map(transformProduct);
}
function transformProduct(raw: any): Product {
return {
id: raw.id,
name: raw.name,
price: parseFloat(raw.price),
image: raw.image_url,
createdAt: new Date(raw.created_at),
};
}
Design Patterns Used
Provider Pattern
// Crear provider con createContextHook
export const [ThemeProvider, useTheme] = createContextHook(() => {
const [mode, setMode] = useState<'light' | 'dark' | 'system'>('system');
const colors = mode === 'dark' ? DarkColors : LightColors;
const isDark = mode === 'dark';
return { mode, setMode, colors, isDark };
});
// Usar en _layout.tsx
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<AppProvider>
<RootLayoutNav />
</AppProvider>
</ThemeProvider>
</QueryClientProvider>
Compound Components
// Componente compuesto para Cards
function Card({ children, style }: CardProps) {
return <View style={[styles.card, style]}>{children}</View>;
}
Card.Image = function CardImage({ source }: { source: string }) {
return <Image source={{ uri: source }} style={styles.image} />;
};
Card.Title = function CardTitle({ children }: { children: string }) {
return <Text style={styles.title}>{children}</Text>;
};
Card.Actions = function CardActions({ children }: { children: ReactNode }) {
return <View style={styles.actions}>{children}</View>;
};
// Uso
<Card>
<Card.Image source={product.image} />
<Card.Title>{product.name}</Card.Title>
<Card.Actions>
<Button title="Comprar" onPress={handleBuy} />
</Card.Actions>
</Card>
Render Props / Children as Function
// Componente con render props
interface QueryRendererProps<T> {
query: UseQueryResult<T>;
children: (data: T) => ReactNode;
loadingComponent?: ReactNode;
errorComponent?: ReactNode;
}
function QueryRenderer<T>({
query,
children,
loadingComponent,
errorComponent
}: QueryRendererProps<T>) {
if (query.isLoading) return loadingComponent || <SkeletonLoader />;
if (query.error) return errorComponent || <ErrorState />;
if (!query.data) return null;
return <>{children(query.data)}</>;
}
// Uso
<QueryRenderer query={productsQuery}>
{(products) => (
<FlatList data={products} renderItem={...} />
)}
</QueryRenderer>
Custom Hooks Pattern
// Hook para debounce
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Hook para formularios
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const setValue = useCallback(<K extends keyof T>(key: K, value: T[K]) => {
setValues(prev => ({ ...prev, [key]: value }));
setErrors(prev => ({ ...prev, [key]: undefined }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, [initialValues]);
return { values, errors, setValue, setErrors, reset };
}
Error Handling
Error Boundaries
// components/ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Button } from './Button';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// Aquí puedes enviar a un servicio de logging
}
handleRetry = () => {
this.setState({ hasError: false, error: undefined });
};
render() {
if (this.state.hasError) {
return this.props.fallback || (
<View style={styles.container}>
<Text style={styles.title}>Algo salió mal</Text>
<Text style={styles.message}>{this.state.error?.message}</Text>
<Button title="Reintentar" onPress={this.handleRetry} />
</View>
);
}
return this.props.children;
}
}
Error Handling en Queries
// Configuración global de React Query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 1000 * 60 * 5, // 5 minutos
},
mutations: {
onError: (error) => {
console.error('Mutation error:', error);
// Mostrar toast global de error
},
},
},
});
// En componentes
const productsQuery = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
onError: (error) => {
showToast(error.message, 'error');
},
});
Optimization and Performance
Memoización
// Memoizar componentes costosos
const ProductCard = React.memo(function ProductCard({ product }: Props) {
return (/* ... */);
});
// Memoizar cálculos costosos
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === selectedCategory);
}, [products, selectedCategory]);
// Memoizar callbacks
const handlePress = useCallback(() => {
onItemPress(item.id);
}, [item.id, onItemPress]);
Listas Optimizadas
// FlatList optimizada
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
// Optimizaciones
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
// Extraer renderItem fuera del componente
const renderItem = useCallback(({ item }: { item: Product }) => (
<ProductCard product={item} />
), []);
Lazy Loading
// Carga diferida de pantallas pesadas
const HeavyScreen = React.lazy(() => import('./HeavyScreen'));
function App() {
return (
<Suspense fallback={<SkeletonLoader />}>
<HeavyScreen />
</Suspense>
);
}
Imágenes Optimizadas
// Usar expo-image para mejor performance
import { Image } from 'expo-image';
<Image
source={{ uri: imageUrl }}
style={styles.image}
contentFit="cover"
transition={200}
placeholder={blurhash}
cachePolicy="memory-disk"
/>
Diagrama de Dependencias
┌─────────────────────────────────────────────────────────────────┐
│ app/ │
│ (Screens & Navigation) │
│ Depende de: components/, contexts/, constants/, types/ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ components/ │
│ (UI Components) │
│ Depende de: contexts/, constants/, types/, utils/ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ contexts/ │
│ (State Management) │
│ Depende de: constants/, types/, utils/ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ constants/ + types/ + utils/ │
│ (Configuración, Tipos, Utilidades) │
│ Sin dependencias internas │
└─────────────────────────────────────────────────────────────────┘
Reglas:
- Las capas superiores pueden importar de las inferiores
- Las capas inferiores NO deben importar de las superiores
constants/,types/,utils/son independientes entre sí