π± 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Γ