🔄 State Management Guide
Basic Principles
- useState - For simple local state
- React Query - For server data and cache
- Context + createContextHook - For shared global state
- 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;
// ...
}