🌐 Contexts Guide

Index

  1. Arquitectura de Contextos
  2. AuthContext
  3. DatabaseContext
  4. AIContext
  5. AnalyticsContext
  6. NotificationsContext
  7. PurchasesContext
  8. AppContext
  9. ThemeContext
  10. I18nContext
  11. ToastContext
  12. Creando Nuevos Contextos
  13. Patrones y Mejores Prácticas

Arquitectura de Contextos

Estructura

contexts/
├── AuthContext.tsx        # Autenticación (login, register, social auth)
├── DatabaseContext.tsx    # Base de datos local (AsyncStorage helpers)
├── AIContext.tsx          # IA (texto, objetos, imágenes, STT)
├── AnalyticsContext.tsx   # Analytics (eventos, sesiones)
├── NotificationsContext.tsx # Notificaciones push y locales
├── PurchasesContext.tsx   # Pagos in-app (RevenueCat)
├── AppContext.tsx         # Estado principal (favorites, settings)
├── ThemeContext.tsx       # Sistema de temas (light/dark/system)
├── I18nContext.tsx        # Internacionalización
└── ToastContext.tsx       # Sistema de toasts

Jerarquía de Providers

// app/_layout.tsx
<QueryClientProvider client={queryClient}>
  <ThemeProvider>
    <I18nProvider>
      <AuthProvider>
        <DatabaseProvider>
          <AIProvider>
            <AnalyticsProvider>
              <NotificationsProvider>
                <PurchasesProvider>
                  <AppProvider>
                    <ToastProvider>
                      <RootLayoutNav />
                    </ToastProvider>
                  </AppProvider>
                </PurchasesProvider>
              </NotificationsProvider>
            </AnalyticsProvider>
          </AIProvider>
        </DatabaseProvider>
      </AuthProvider>
    </I18nProvider>
  </ThemeProvider>
</QueryClientProvider>

Orden importante:

  1. QueryClientProvider - Siempre primero (React Query)
  2. ThemeProvider - Colores necesarios en toda la app
  3. I18nProvider - Traducciones necesarias en toda la app
  4. AuthProvider - Autenticación (otros contextos pueden necesitar el usuario)
  5. DatabaseProvider - Base de datos local
  6. AIProvider - Funcionalidades de IA
  7. AnalyticsProvider - Tracking de eventos
  8. NotificationsProvider - Push y notificaciones locales
  9. PurchasesProvider - Pagos in-app
  10. AppProvider - Estado de app (favorites, settings)
  11. ToastProvider - Feedback visual (puede usar otros contextos)

AuthContext

Ubicación: contexts/AuthContext.tsx

Propósito

Maneja autenticación completa: login, registro, logout, social auth (Google, Apple), y gestión de perfil.

Hook: useAuth

import { useAuth } from '@/contexts/AuthContext';

function MyComponent() {
  const {
    // Estado
    user,              // User | null
    token,             // string | null
    isLoading,         // boolean
    isAuthenticated,   // boolean

    // Auth básico
    login,             // (credentials) => Promise
    register,          // (data) => Promise
    logout,            // () => Promise
    updateProfile,     // (updates) => Promise
    resetPassword,     // (email) => Promise

    // Social Auth
    signInWithGoogle,  // () => Promise
    signInWithApple,   // () => Promise

    // Estados de carga
    isLoggingIn,
    isRegistering,
    isLoggingOut,
    isGoogleLoading,
    isAppleLoading,

    // Errores
    loginError,
    registerError,
    googleError,
    appleError,
  } = useAuth();
}

Hooks Derivados

// Verificar si requiere login
import { useRequireAuth } from '@/contexts/AuthContext';

const { isAuthenticated, isLoading, requiresLogin } = useRequireAuth();

// Solo social auth
import { useSocialAuth } from '@/contexts/AuthContext';

const { signInWithGoogle, signInWithApple, isLoading } = useSocialAuth();

Ejemplo: Pantalla de Login

function LoginScreen() {
  const { login, signInWithGoogle, signInWithApple, isLoggingIn, isGoogleLoading } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      await login({ email, password });
      router.replace('/(tabs)');
    } catch (error) {
      showToast('Error al iniciar sesión', 'error');
    }
  };

  return (
    <View>
      <Input value={email} onChangeText={setEmail} placeholder="Email" />
      <Input value={password} onChangeText={setPassword} secureTextEntry />
      <Button title="Iniciar sesión" onPress={handleLogin} loading={isLoggingIn} />
      
      <Button title="Continuar con Google" onPress={signInWithGoogle} loading={isGoogleLoading} />
      <Button title="Continuar con Apple" onPress={signInWithApple} />
    </View>
  );
}

DatabaseContext

Ubicación: contexts/DatabaseContext.tsx

Propósito

Helpers para base de datos local con AsyncStorage. Provee operaciones CRUD, colecciones, y valores persistidos.

Hook: useDatabase

import { useDatabase } from '@/contexts/DatabaseContext';

const {
  // Operaciones básicas
  getItem,           // <T>(key) => Promise<T | null>
  setItem,           // <T>(key, value) => Promise<void>
  removeItem,        // (key) => Promise<void>
  getAllKeys,        // (prefix?) => Promise<string[]>

  // Colecciones
  getCollection,     // <T>(name) => Promise<DatabaseItem<T>[]>
  addToCollection,   // <T>(name, data) => Promise<DatabaseItem<T>>
  updateInCollection,// <T>(name, id, data) => Promise<DatabaseItem<T>>
  removeFromCollection, // (name, id) => Promise<void>
  clearCollection,   // (name) => Promise<void>
  clearAll,          // () => Promise<void>
} = useDatabase();

Hook: useCollection

import { useCollection } from '@/contexts/DatabaseContext';

function TodoList() {
  const {
    items,      // DatabaseItem<Todo>[]
    isLoading,
    add,        // (data: Todo) => Promise
    update,     // ({ id, data }) => Promise
    remove,     // (id) => Promise
    clear,      // () => Promise
    isAdding,
    isUpdating,
    isRemoving,
  } = useCollection<Todo>('todos');

  return (
    <FlatList
      data={items}
      renderItem={({ item }) => (
        <TodoItem 
          todo={item.data} 
          onDelete={() => remove(item.id)} 
        />
      )}
    />
  );
}

Hook: useStoredValue

import { useStoredValue } from '@/contexts/DatabaseContext';

function Settings() {
  const {
    value,      // T
    isLoading,
    setValue,   // (value: T) => Promise
    remove,     // () => Promise
  } = useStoredValue<boolean>('dark_mode', false);

  return (
    <Switch value={value} onValueChange={setValue} />
  );
}

AIContext

Ubicación: contexts/AIContext.tsx

Propósito

Integración con IA: generación de texto, objetos estructurados, imágenes, y speech-to-text.

Hook: useAI

import { useAI } from '@/contexts/AIContext';

const {
  // Funciones
  generateText,      // (prompt | messages) => Promise<string>
  generateObject,    // <T>(messages, schema) => Promise<T>
  generateImage,     // (prompt, size?) => Promise<ImageResponse>
  editImage,         // (prompt, images, aspectRatio?) => Promise<ImageResponse>
  transcribeAudio,   // (audioUri, language?) => Promise<STTResponse>

  // Estados de carga
  isGeneratingText,
  isGeneratingObject,
  isGeneratingImage,
  isEditingImage,
  isTranscribing,
  isLoading,         // any operation loading

  // Errores
  lastError,
  clearError,
} = useAI();

Hooks Derivados

import { useTextGeneration, useObjectGeneration, useImageGeneration, useSpeechToText } from '@/contexts/AIContext';

// Solo texto
const { generateText, isLoading, error } = useTextGeneration();

// Solo objetos
const { generateObject, isLoading, error } = useObjectGeneration();

// Solo imágenes
const { generateImage, editImage, isGenerating, isEditing } = useImageGeneration();

// Solo STT
const { transcribeAudio, isLoading, error } = useSpeechToText();

Ejemplo: Generar Descripción

import { z } from 'zod';

function ProductForm() {
  const { generateObject, isGeneratingObject } = useAI();
  const [title, setTitle] = useState('');

  const autoGenerateDescription = async () => {
    const result = await generateObject(
      [{ role: 'user', content: `Genera una descripción para: ${title}` }],
      z.object({
        description: z.string(),
        tags: z.array(z.string()),
      })
    );
    setDescription(result.description);
    setTags(result.tags);
  };

  return (
    <View>
      <Input value={title} onChangeText={setTitle} />
      <Button 
        title="Generar con IA" 
        onPress={autoGenerateDescription} 
        loading={isGeneratingObject} 
      />
    </View>
  );
}

AnalyticsContext

Ubicación: contexts/AnalyticsContext.tsx

Propósito

Tracking de eventos, sesiones, y comportamiento del usuario.

Hook: useAnalytics

import { useAnalytics } from '@/contexts/AnalyticsContext';

const {
  // Tracking básico
  track,           // (name, properties?) => void
  trackScreen,     // (screenName, properties?) => void
  trackButton,     // (buttonName, properties?) => void

  // Eventos específicos
  trackPurchase,   // ({ productId, price, currency }) => void
  trackSignUp,     // (method) => void
  trackLogin,      // (method) => void
  trackSearch,     // (query, resultsCount?) => void
  trackShare,      // (contentType, itemId, method) => void
  trackError,      // (error, context?) => void
  trackFeatureUsed,// (featureName, properties?) => void

  // Usuario
  identify,        // (userId, traits?) => Promise
  reset,           // () => Promise
  flush,           // () => Promise
} = useAnalytics();

Hook: useScreenTracking

import { useScreenTracking } from '@/contexts/AnalyticsContext';

function HomeScreen() {
  useScreenTracking('Home');
  // ...
}

Ejemplo: Tracking en Acciones

function ProductCard({ product }) {
  const { track, trackButton } = useAnalytics();

  const handleAddToCart = () => {
    trackButton('add_to_cart', { product_id: product.id });
    addToCart(product);
  };

  const handleShare = () => {
    track('share_initiated', { product_id: product.id });
    Share.share({ url: product.url });
  };

  return <Card onPress={handleAddToCart} />;
}

NotificationsContext

Ubicación: contexts/NotificationsContext.tsx

Propósito

Notificaciones push y locales con expo-notifications.

Hook: useNotifications

import { useNotifications } from '@/contexts/NotificationsContext';

const {
  // Estado
  notifications,     // NotificationItem[]
  unreadCount,       // number
  hasPermission,     // boolean
  pushToken,         // string | null
  isLoading,

  // Registro
  registerPush,      // () => Promise<string | null>
  isRegistering,

  // Gestión
  addNotification,   // (notification) => void
  markAsRead,        // (id) => void
  markAllAsRead,     // () => void
  clearAll,          // () => void

  // Locales
  scheduleLocal,     // (title, body, trigger?, data?) => Promise<string>
  cancelScheduled,   // (id) => Promise
  cancelAllScheduled,// () => Promise
} = useNotifications();

Ejemplo: Solicitar Permisos

function NotificationSettings() {
  const { hasPermission, registerPush, isRegistering } = useNotifications();

  if (hasPermission) {
    return <Text>Notificaciones activadas ✓</Text>;
  }

  return (
    <Button
      title="Activar notificaciones"
      onPress={registerPush}
      loading={isRegistering}
    />
  );
}

Ejemplo: Notificación Local

function ReminderButton() {
  const { scheduleLocal } = useNotifications();

  const setReminder = async () => {
    await scheduleLocal(
      'Recordatorio',
      'No olvides completar tu tarea',
      { seconds: 3600 }, // en 1 hora
      { route: '/tasks' }
    );
    showToast('Recordatorio programado', 'success');
  };

  return <Button title="Recordar en 1 hora" onPress={setReminder} />;
}

PurchasesContext

Ubicación: contexts/PurchasesContext.tsx

Propósito

Pagos in-app con RevenueCat: suscripciones, compras, y verificación de entitlements.

Configuración

Requiere variables de entorno:

EXPO_PUBLIC_REVENUECAT_IOS_API_KEY=appl_xxx
EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY=goog_xxx
EXPO_PUBLIC_REVENUECAT_TEST_API_KEY=xxx (para desarrollo)

Hook: usePurchases

import { usePurchases } from '@/contexts/PurchasesContext';

const {
  // Estado
  isConfigured,      // boolean
  isLoading,
  customerInfo,      // CustomerInfo
  offerings,         // PurchasesOfferings

  // Verificación
  isPremium,         // (entitlement?) => boolean
  hasEntitlement,    // (entitlement) => boolean
  getActiveEntitlements, // () => string[]

  // Paquetes
  currentPackages,   // PurchasesPackage[]
  monthlyPackage,
  annualPackage,
  lifetimePackage,
  weeklyPackage,

  // Compras
  purchase,          // (package) => Promise
  isPurchasing,
  purchaseError,

  // Restaurar
  restore,           // () => Promise
  isRestoring,

  // Usuario
  loginUser,         // (userId) => Promise
  logoutUser,        // () => Promise
  getManagementURL,  // () => Promise<string | null>
} = usePurchases();

Hooks Derivados

import { usePremiumGuard, usePremiumFeature } from '@/contexts/PurchasesContext';

// Verificar acceso premium
const { isPremium, isLoading, requiresPremium } = usePremiumGuard();

// Wrap funciones con verificación
const { withPremiumCheck } = usePremiumFeature();
const handleExport = withPremiumCheck(
  () => exportData(),
  () => showPaywall()
);

Ejemplo: Paywall

function PaywallScreen() {
  const { currentPackages, purchase, isPurchasing, restore } = usePurchases();

  return (
    <View>
      <Text>Desbloquea todas las funciones</Text>
      
      {currentPackages.map(pkg => (
        <TouchableOpacity
          key={pkg.identifier}
          onPress={() => purchase(pkg)}
          disabled={isPurchasing}
        >
          <Text>{pkg.product.title}</Text>
          <Text>{pkg.product.priceString}</Text>
        </TouchableOpacity>
      ))}

      <Button title="Restaurar compras" onPress={restore} />
    </View>
  );
}

AppContext

Ubicación: contexts/AppContext.tsx

Propósito

Maneja el estado global principal de la aplicación: usuario, favoritos, notificaciones y configuraciones.

Estado

interface AppState {
  user: User | null;
  favorites: string[];
  notifications: Notification[];
  settings: {
    pushNotifications: boolean;
    hapticFeedback: boolean;
  };
}

Hook: useApp

import { useApp } from '@/contexts/AppContext';

function MyComponent() {
  const {
    // Estado
    user,              // User | null
    favorites,         // string[]
    notifications,     // Notification[]
    settings,          // Settings
    isLoading,         // boolean
    
    // Acciones de Usuario
    setUser,           // (user: User | null) => void
    
    // Acciones de Favoritos
    toggleFavorite,    // (itemId: string) => void
    isFavorite,        // (itemId: string) => boolean
    
    // Acciones de Notificaciones
    addNotification,         // (notification) => void
    markNotificationAsRead,  // (id: string) => void
    markAllAsRead,           // () => void
    clearNotifications,      // () => void
    unreadCount,             // number
    
    // Acciones de Settings
    updateSettings,    // (settings: Partial<Settings>) => void
  } = useApp();
}

Ejemplos de Uso

Mostrar información del usuario:

function ProfileHeader() {
  const { user } = useApp();
  
  if (!user) {
    return <LoginPrompt />;
  }
  
  return (
    <View>
      <Avatar source={user.avatar} name={user.name} />
      <Text>{user.name}</Text>
      <Text>{user.email}</Text>
    </View>
  );
}

Manejar favoritos:

function ItemCard({ item }) {
  const { toggleFavorite, isFavorite } = useApp();
  const isItemFavorite = isFavorite(item.id);
  
  return (
    <Card>
      <Text>{item.title}</Text>
      <TouchableOpacity onPress={() => toggleFavorite(item.id)}>
        <Heart fill={isItemFavorite ? 'red' : 'transparent'} />
      </TouchableOpacity>
    </Card>
  );
}

Badge de notificaciones:

function NotificationBadge() {
  const { unreadCount } = useApp();
  
  if (unreadCount === 0) return null;
  
  return (
    <View style={styles.badge}>
      <Text>{unreadCount > 99 ? '99+' : unreadCount}</Text>
    </View>
  );
}

ThemeContext

Ubicación: contexts/ThemeContext.tsx

Propósito

Maneja el sistema de temas con soporte para modo claro, oscuro y automático (sistema).

Modos de Tema

ModoDescripción
lightTema claro siempre
darkTema oscuro siempre
systemSigue configuración del dispositivo

Hook: useTheme

import { useTheme } from '@/contexts/ThemeContext';

function MyComponent() {
  const {
    // Estado
    colors,      // Objeto con todos los colores
    isDark,      // boolean - true si modo oscuro activo
    themeMode,   // 'light' | 'dark' | 'system'
    
    // Acciones
    setTheme,    // (mode: 'light' | 'dark' | 'system') => void
    toggleTheme, // () => void - cicla entre modos
  } = useTheme();
}

Colores Disponibles

colors = {
  // Primarios
  primary: string,
  primaryLight: string,
  primaryDark: string,
  
  // Secundarios y Acento
  secondary: string,
  secondaryLight: string,
  accent: string,
  accentLight: string,
  
  // Estados
  success: string,
  successLight: string,
  warning: string,
  warningLight: string,
  error: string,
  errorLight: string,
  
  // Fondos
  background: string,
  surface: string,
  surfaceSecondary: string,
  
  // Texto
  text: string,
  textSecondary: string,
  textTertiary: string,
  textInverse: string,
  
  // Bordes
  border: string,
  borderLight: string,
  
  // Overlay
  overlay: string,
}

Ejemplos de Uso

Aplicar colores dinámicos:

function Card({ title, description }) {
  const { colors } = useTheme();
  
  return (
    <View style={[styles.card, { backgroundColor: colors.surface }]}>
      <Text style={[styles.title, { color: colors.text }]}>
        {title}
      </Text>
      <Text style={[styles.description, { color: colors.textSecondary }]}>
        {description}
      </Text>
    </View>
  );
}

Toggle de tema en Settings:

function ThemeSetting() {
  const { themeMode, setTheme } = useTheme();
  
  const options = [
    { label: 'Claro', value: 'light' },
    { label: 'Oscuro', value: 'dark' },
    { label: 'Sistema', value: 'system' },
  ];
  
  return (
    <View>
      {options.map(option => (
        <TouchableOpacity
          key={option.value}
          onPress={() => setTheme(option.value)}
          style={[
            styles.option,
            themeMode === option.value && styles.optionSelected,
          ]}
        >
          <Text>{option.label}</Text>
        </TouchableOpacity>
      ))}
    </View>
  );
}

Icono de tema:

function ThemeIcon() {
  const { isDark, toggleTheme } = useTheme();
  
  return (
    <TouchableOpacity onPress={toggleTheme}>
      {isDark ? <Sun size={24} /> : <Moon size={24} />}
    </TouchableOpacity>
  );
}

I18nContext

Ubicación: contexts/I18nContext.tsx

Propósito

Maneja la internacionalización con soporte para múltiples idiomas.

Idiomas Soportados

CódigoIdioma
esEspañol (default)
enEnglish

Hook: useI18n

import { useI18n } from '@/contexts/I18nContext';

function MyComponent() {
  const {
    // Estado
    t,           // Objeto con todas las traducciones
    locale,      // 'es' | 'en' - idioma actual
    
    // Acciones
    setLocale,   // (locale: 'es' | 'en') => void
  } = useI18n();
}

Estructura de Traducciones

t = {
  common: {
    save: string,
    cancel: string,
    delete: string,
    edit: string,
    loading: string,
    error: string,
    success: string,
    retry: string,
    search: string,
    noResults: string,
    close: string,
    confirm: string,
    back: string,
    next: string,
    done: string,
  },
  
  tabs: {
    home: string,
    explore: string,
    activity: string,
    messages: string,
    favorites: string,
    profile: string,
  },
  
  home: {
    welcomeBack: string,
    overview: string,
    recentItems: string,
    categories: string,
    viewAll: string,
  },
  
  explore: {
    title: string,
    searchPlaceholder: string,
    filters: string,
    sortBy: string,
    noResults: string,
  },
  
  // ... más secciones
}

Ejemplos de Uso

Textos básicos:

function WelcomeMessage() {
  const { t } = useI18n();
  const { user } = useApp();
  
  return (
    <Text>{t.home.welcomeBack}, {user?.name}!</Text>
  );
}

Botones y acciones:

function ActionButtons({ onSave, onCancel }) {
  const { t } = useI18n();
  
  return (
    <View style={styles.buttons}>
      <Button title={t.common.cancel} onPress={onCancel} variant="outline" />
      <Button title={t.common.save} onPress={onSave} variant="primary" />
    </View>
  );
}

Selector de idioma:

function LanguageSetting() {
  const { locale, setLocale } = useI18n();
  
  const languages = [
    { code: 'es', name: 'Español', flag: '🇪🇸' },
    { code: 'en', name: 'English', flag: '🇺🇸' },
  ];
  
  return (
    <View>
      {languages.map(lang => (
        <TouchableOpacity
          key={lang.code}
          onPress={() => setLocale(lang.code)}
          style={[
            styles.langOption,
            locale === lang.code && styles.langSelected,
          ]}
        >
          <Text>{lang.flag} {lang.name}</Text>
          {locale === lang.code && <Check size={20} />}
        </TouchableOpacity>
      ))}
    </View>
  );
}

ToastContext

Ubicación: contexts/ToastContext.tsx

Propósito

Sistema de notificaciones temporales (toasts) para feedback al usuario.

Tipos de Toast

TipoColorUso
successVerdeOperación exitosa
errorRojoError o fallo
warningNaranjaAdvertencia
infoAzulInformación general

Hook: useToast

import { useToast } from '@/contexts/ToastContext';

function MyComponent() {
  const { showToast } = useToast();
  
  // Mostrar toast
  showToast('Mensaje', 'success');
  showToast('Error occurred', 'error');
  showToast('Be careful!', 'warning');
  showToast('FYI', 'info');
}

Ejemplos de Uso

Después de guardar:

async function handleSave() {
  const { showToast } = useToast();
  
  try {
    await saveData();
    showToast('Guardado exitosamente', 'success');
  } catch (error) {
    showToast('Error al guardar', 'error');
  }
}

Con haptic feedback:

async function handleDelete() {
  const { showToast } = useToast();
  
  try {
    await deleteItem();
    triggerHaptic('success');
    showToast('Eliminado', 'success');
  } catch (error) {
    triggerHaptic('error');
    showToast('No se pudo eliminar', 'error');
  }
}

Creando Nuevos Contextos

Usando createContextHook

// contexts/CartContext.tsx
import createContextHook from '@nkzw/create-context-hook';
import { useState, useCallback, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import AsyncStorage from '@react-native-async-storage/async-storage';

// 1. Definir tipos
interface CartItem {
  id: string;
  productId: string;
  quantity: number;
  price: number;
}

const STORAGE_KEY = 'cart';

// 2. Crear contexto con createContextHook
export const [CartProvider, useCart] = createContextHook(() => {
  const queryClient = useQueryClient();
  const [items, setItems] = useState<CartItem[]>([]);

  // 3. Query para cargar datos persistidos
  const cartQuery = useQuery({
    queryKey: ['cart'],
    queryFn: async () => {
      const stored = await AsyncStorage.getItem(STORAGE_KEY);
      return stored ? JSON.parse(stored) : [];
    },
  });

  // 4. Sincronizar con query
  useEffect(() => {
    if (cartQuery.data) {
      setItems(cartQuery.data);
    }
  }, [cartQuery.data]);

  // 5. Mutation para persistir
  const saveMutation = useMutation({
    mutationFn: async (newItems: CartItem[]) => {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newItems));
      return newItems;
    },
    onSuccess: (data) => {
      queryClient.setQueryData(['cart'], data);
    },
  });

  // 6. Acciones
  const addItem = useCallback((item: Omit<CartItem, 'id'>) => {
    const newItem = { ...item, id: Date.now().toString() };
    const newItems = [...items, newItem];
    setItems(newItems);
    saveMutation.mutate(newItems);
  }, [items, saveMutation]);

  const removeItem = useCallback((itemId: string) => {
    const newItems = items.filter(i => i.id !== itemId);
    setItems(newItems);
    saveMutation.mutate(newItems);
  }, [items, saveMutation]);

  const clearCart = useCallback(() => {
    setItems([]);
    saveMutation.mutate([]);
  }, [saveMutation]);

  // 7. Valores derivados
  const totalItems = useMemo(() => 
    items.reduce((sum, item) => sum + item.quantity, 0),
    [items]
  );

  const totalPrice = useMemo(() =>
    items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    [items]
  );

  // 8. Retornar API del contexto
  return {
    items,
    isLoading: cartQuery.isLoading,
    addItem,
    removeItem,
    clearCart,
    totalItems,
    totalPrice,
  };
});

// 9. Hooks derivados (opcional)
export function useCartItem(productId: string) {
  const { items } = useCart();
  return useMemo(
    () => items.find(i => i.productId === productId),
    [items, productId]
  );
}

Agregar al Layout

// app/_layout.tsx
import { CartProvider } from '@/contexts/CartContext';

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider>
        <AppProvider>
          <CartProvider>
            <RootLayoutNav />
          </CartProvider>
        </AppProvider>
      </ThemeProvider>
    </QueryClientProvider>
  );
}

Patrones y Mejores Prácticas

1. Separar Estado por Dominio

// ✅ BIEN: Contextos separados por dominio
<AuthProvider>      // Solo autenticación
<CartProvider>      // Solo carrito
<SettingsProvider>  // Solo configuración

// ❌ MAL: Un mega contexto con todo
<AppProvider>  // auth + cart + settings + notifications + ...

2. No Persistir Todo

// ✅ BIEN: Solo persistir lo necesario
const settingsQuery = useQuery({
  queryKey: ['settings'],
  queryFn: async () => {
    const stored = await AsyncStorage.getItem('settings');
    return stored ? JSON.parse(stored) : defaultSettings;
  },
});

// ❌ MAL: Persistir estado temporal
// No guardar: filtros de búsqueda, modal abierto/cerrado, etc.

3. Usar useMemo y useCallback

// ✅ BIEN: Memoizar valores derivados y funciones
const totalItems = useMemo(() => 
  items.reduce((sum, i) => sum + i.quantity, 0),
  [items]
);

const addItem = useCallback((item) => {
  // ...
}, [items]);

// ❌ MAL: Crear nuevas funciones/objetos en cada render
const totalItems = items.reduce((sum, i) => sum + i.quantity, 0);
const addItem = (item) => { /* ... */ };

4. Hooks Derivados para Lógica Específica

// Hook principal del contexto
export const [CartProvider, useCart] = createContextHook(() => {
  // ... estado completo del carrito
});

// Hook derivado para un producto específico
export function useProductInCart(productId: string) {
  const { items, addItem, removeItem } = useCart();
  
  const item = useMemo(
    () => items.find(i => i.productId === productId),
    [items, productId]
  );
  
  return {
    isInCart: !!item,
    quantity: item?.quantity ?? 0,
    add: () => addItem({ productId, quantity: 1, price: 0 }),
    remove: () => item && removeItem(item.id),
  };
}

5. Manejo de Errores

const saveMutation = useMutation({
  mutationFn: async (data) => {
    await AsyncStorage.setItem(KEY, JSON.stringify(data));
    return data;
  },
  onError: (error) => {
    console.error('Error saving:', error);
    showToast('Error al guardar', 'error');
  },
  onSuccess: (data) => {
    queryClient.setQueryData([KEY], data);
  },
});

Checklist de Contextos

  • Tipos TypeScript definidos
  • Usar createContextHook de @nkzw/create-context-hook
  • React Query para datos remotos/persistidos
  • useMemo para valores derivados
  • useCallback para funciones
  • Hooks derivados para casos específicos
  • Manejo de errores
  • Documentación de la API