🔄 State Management Guide

Basic Principles

  1. useState - For simple local state
  2. React Query - For server data and cache
  3. Context + createContextHook - For shared global state
  4. AsyncStorage - For local persistence

1. Local State (useState)

For state that only one component needs:

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [isExpanded, setIsExpanded] = useState(false);
  
  return (
    <Input 
      value={query} 
      onChangeText={setQuery}
      placeholder="Search..."
    />
  );
}

2. React Query (Server Data)

Data fetching (useQuery)

import { useQuery } from '@tanstack/react-query';

function ProductList() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const response = await fetch('/api/products');
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutos
  });

  if (isLoading) return <SkeletonLoader />;
  if (error) return <ErrorState />;
  
  return <FlatList data={data} ... />;
}

Mutaciones (useMutation)

import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddProductButton() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: async (newProduct: Product) => {
      const response = await fetch('/api/products', {
        method: 'POST',
        body: JSON.stringify(newProduct),
      });
      return response.json();
    },
    onSuccess: () => {
      // Invalidar caché para refetch
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
    onError: (error) => {
      showToast('Error al crear producto', 'error');
    },
  });

  return (
    <Button 
      title="Agregar" 
      onPress={() => mutation.mutate(productData)}
      loading={mutation.isPending}
    />
  );
}

Query Keys

// Ejemplos de query keys
['products']                    // Lista de productos
['products', { category: 'tech' }] // Productos filtrados
['product', id]                 // Producto individual
['user', userId, 'orders']      // Órdenes de un usuario

3. Contexto Global (createContextHook)

Crear un Contexto

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

interface CartItem {
  id: string;
  productId: string;
  quantity: number;
}

const STORAGE_KEY = 'cart_items';

export const [CartProvider, useCart] = createContextHook(() => {
  const queryClient = useQueryClient();
  const [items, setItems] = useState<CartItem[]>([]);

  // Cargar del storage
  const cartQuery = useQuery({
    queryKey: ['cart'],
    queryFn: async () => {
      const stored = await AsyncStorage.getItem(STORAGE_KEY);
      return stored ? JSON.parse(stored) : [];
    },
  });

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

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

  // Acciones
  const addItem = useCallback((productId: string) => {
    const existing = items.find(i => i.productId === productId);
    let newItems: CartItem[];
    
    if (existing) {
      newItems = items.map(i => 
        i.productId === productId 
          ? { ...i, quantity: i.quantity + 1 }
          : i
      );
    } else {
      newItems = [...items, { 
        id: Date.now().toString(), 
        productId, 
        quantity: 1 
      }];
    }
    
    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]);

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

  return {
    items,
    isLoading: cartQuery.isLoading,
    addItem,
    removeItem,
    clearCart,
    totalItems,
  };
});

// Hook derivado para item específico
export function useCartItem(productId: string) {
  const { items } = useCart();
  return useMemo(
    () => items.find(i => i.productId === productId),
    [items, productId]
  );
}

Usar el Contexto

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

export default function Layout() {
  return (
    <QueryClientProvider client={queryClient}>
      <CartProvider>
        <Stack />
      </CartProvider>
    </QueryClientProvider>
  );
}

// En componentes
function ProductCard({ product }) {
  const { addItem } = useCart();
  const cartItem = useCartItem(product.id);
  
  return (
    <Card>
      <Text>{product.name}</Text>
      <Button 
        title={cartItem ? `En carrito (${cartItem.quantity})` : 'Agregar'}
        onPress={() => addItem(product.id)}
      />
    </Card>
  );
}

4. Persistencia con AsyncStorage

Guardar/Leer Datos

import AsyncStorage from '@react-native-async-storage/async-storage';

// Guardar
await AsyncStorage.setItem('user_preferences', JSON.stringify({
  theme: 'dark',
  notifications: true,
}));

// Leer
const stored = await AsyncStorage.getItem('user_preferences');
const preferences = stored ? JSON.parse(stored) : defaultPreferences;

// Eliminar
await AsyncStorage.removeItem('user_preferences');

// Limpiar todo
await AsyncStorage.clear();

Con React Query

const preferencesQuery = useQuery({
  queryKey: ['preferences'],
  queryFn: async () => {
    const stored = await AsyncStorage.getItem('preferences');
    return stored ? JSON.parse(stored) : defaultPreferences;
  },
});

Patrones Avanzados

Optimistic Updates

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // Cancelar queries en progreso
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    
    // Guardar estado anterior
    const previousTodos = queryClient.getQueryData(['todos']);
    
    // Actualizar optimisticamente
    queryClient.setQueryData(['todos'], (old) => 
      old.map(todo => todo.id === newTodo.id ? newTodo : todo)
    );
    
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // Revertir si hay error
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    // Refetch para sincronizar
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Combinar Múltiples Queries

function Dashboard() {
  const userQuery = useQuery({ queryKey: ['user'], queryFn: fetchUser });
  const ordersQuery = useQuery({ 
    queryKey: ['orders', userQuery.data?.id], 
    queryFn: () => fetchOrders(userQuery.data.id),
    enabled: !!userQuery.data?.id, // Solo ejecutar si hay user
  });
  
  const isLoading = userQuery.isLoading || ordersQuery.isLoading;
  
  // ...
}