🌐 Contexts Guide
Index
- Arquitectura de Contextos
- AuthContext
- DatabaseContext
- AIContext
- AnalyticsContext
- NotificationsContext
- PurchasesContext
- AppContext
- ThemeContext
- I18nContext
- ToastContext
- Creando Nuevos Contextos
- 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:
QueryClientProvider- Siempre primero (React Query)ThemeProvider- Colores necesarios en toda la appI18nProvider- Traducciones necesarias en toda la appAuthProvider- Autenticación (otros contextos pueden necesitar el usuario)DatabaseProvider- Base de datos localAIProvider- Funcionalidades de IAAnalyticsProvider- Tracking de eventosNotificationsProvider- Push y notificaciones localesPurchasesProvider- Pagos in-appAppProvider- Estado de app (favorites, settings)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
| Modo | Descripción |
|---|---|
light | Tema claro siempre |
dark | Tema oscuro siempre |
system | Sigue 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ódigo | Idioma |
|---|---|
es | Español (default) |
en | English |
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
| Tipo | Color | Uso |
|---|---|---|
success | Verde | Operación exitosa |
error | Rojo | Error o fallo |
warning | Naranja | Advertencia |
info | Azul | Informació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
createContextHookde@nkzw/create-context-hook - React Query para datos remotos/persistidos
-
useMemopara valores derivados -
useCallbackpara funciones - Hooks derivados para casos específicos
- Manejo de errores
- Documentación de la API