📱 Application Architecture

Index

  1. Estructura del Proyecto
  2. Patrones de Arquitectura
  3. Flujo de Datos
  4. Convenciones de Código
  5. Capas de la Aplicación
  6. Patrones de Diseño Utilizados
  7. Manejo de Errores
  8. 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 EstadoSoluciónEjemplo
Estado de UI localuseStateModal abierto/cerrado, input value
Estado de servidorReact QueryLista de productos, datos de usuario
Estado global de appContext + createContextHookUsuario actual, tema, idioma
Persistencia localAsyncStoragePreferencias, 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

TipoConvenciónEjemplo
ComponentesPascalCaseButton.tsx, ProductCard.tsx
HookscamelCase con "use"useDebounce.ts, useAuth.ts
ContextosPascalCase con "Context"AppContext.tsx, ThemeContext.tsx
UtilidadescamelCasehelpers.ts, formatters.ts
TiposPascalCaseindex.ts (dentro de types/)
ConstantescamelCasecolors.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í