Gestión de Estado y Caché de Red en React Native con Zustand y TanStack Query

En el desarrollo de aplicaciones móviles con React Native y Expo, uno de los errores arquitectónicos más comunes es mezclar el estado del cliente (temas, modales abiertos, filtros de búsqueda, credenciales de sesión) con el estado del servidor (catálogo de productos, notificaciones, historial de órdenes). Esta sobrecarga en gestores globales tradicionales como Redux produce código redundante (boilerplate), problemas de re-renderizados innecesarios y sincronizaciones manuales propensas a errores.

La Regla de Oro: Separación de Responsabilidades#
Para construir aplicaciones móviles ultra fluidas a 60/120 FPS debemos diferenciar claramente dos dominios:
+-----------------------------------------------------------------------------------+
| ARQUITECTURA DE DATOS EN LA APP |
+-----------------------------------------------------------------------------------+
| |
| +-------------------------------------+ +-----------------------------------+ |
| | ESTADO DE CLIENTE (UI STATE) | | ESTADO DE SERVIDOR (ASYNC) | |
| | Herramienta: ZUSTAND | | Herramienta: TANSTACK QUERY | |
| +-------------------------------------+ +-----------------------------------+ |
| | • Modo oscuro / Tema visual | | • Caché persistente de peticiones | |
| | • Tokens de sesión & perfil activo | | • Revalidación en segundo plano | |
| | • Estado de modales y drawers | | • Mutaciones optimistas | |
| | • Filtros locales de listados | | • Paginación infinita | |
| +-------------------------------------+ +-----------------------------------+ |
| |
+-----------------------------------------------------------------------------------+
1. Estado de Cliente con Zustand: Ligero y Reactivo#
Zustand es una librería minimalista basada en hooks que no requiere envolver la aplicación en providers engorrosos y permite suscripciones atómicas a propiedades específicas.
Store de Autenticación y UI (src/stores/useAuthStore.ts)#
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'client';
}
interface AuthState {
token: string | null;
user: User | null;
isAuthenticated: boolean;
theme: 'light' | 'dark' | 'system';
setAuth: (token: string, user: User) => void;
logout: () => void;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isAuthenticated: false,
theme: 'system',
setAuth: (token, user) => set({ token, user, isAuthenticated: true }),
logout: () => set({ token: null, user: null, isAuthenticated: false }),
setTheme: (theme) => set({ theme }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
Suscripción Atómica sin Re-renders Innecesarios#
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useAuthStore } from '@/stores/useAuthStore';
export function UserHeader() {
// Solo se suscribe al nombre del usuario. No re-renderizará si cambia el tema.
const userName = useAuthStore((state) => state.user?.name);
const logout = useAuthStore((state) => state.logout);
return (
<View className="flex-row justify-between items-center p-4 bg-slate-900">
<Text className="text-white font-bold text-lg">Hola, {userName || 'Invitado'}</Text>
<TouchableOpacity onPress={logout} className="bg-rose-600 px-3 py-1.5 rounded-lg">
<Text className="text-white text-xs font-semibold">Cerrar Sesión</Text>
</TouchableOpacity>
</View>
);
}
2. Estado de Servidor con TanStack Query (React Query v5)#
TanStack Query gestiona automáticamente el ciclo de vida de las peticiones de red: reintentos tras caídas de conexión, deduplicación de llamadas simultáneas y stale-while-revalidate.
Configuración del QueryClient con Persistencia Offline#
// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutos de validez en memoria
gcTime: 1000 * 60 * 60 * 24, // 24 horas en caché recolector
retry: 2,
refetchOnReconnect: true,
},
},
});
Hook Personalizado para Consultar Productos#
// src/hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
export function useProducts(category?: string) {
return useQuery<Product[]>({
queryKey: ['products', { category }],
queryFn: async () => {
const { data } = await axios.get('/api/products', {
params: { category },
});
return data;
},
});
}
Mutación Optimista para Agregar al Carrito#
Las actualizaciones optimistas reflejan el cambio en la UI instantáneamente antes de que el servidor responda, revirtiéndolo solo si ocurre un fallo de red:
// src/hooks/useAddToCart.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useAddToCart() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (productId: string) => {
return axios.post('/api/cart/items', { productId, quantity: 1 });
},
onMutate: async (productId) => {
// 1. Cancelar refetches salientes
await queryClient.cancelQueries({ queryKey: ['cart'] });
// 2. Tomar captura del estado previo
const previousCart = queryClient.getQueryData(['cart']);
// 3. Actualizar la caché de forma optimista
queryClient.setQueryData(['cart'], (old: any) => ({
...old,
itemsCount: (old?.itemsCount || 0) + 1,
}));
// Retornar contexto de rollback
return { previousCart };
},
onError: (err, productId, context) => {
// Revertir a la captura previa en caso de error
if (context?.previousCart) {
queryClient.setQueryData(['cart'], context.previousCart);
}
},
onSettled: () => {
// Revalidar para asegurar sincronización con la base de datos
queryClient.invalidateQueries({ queryKey: ['cart'] });
},
});
}
Comparativa de Rendimiento#
| Criterio | Redux + Thunks Tradicional | Zustand + TanStack Query |
|---|---|---|
| Tamaño de Bundle (JS) | ~30 KB + dependencias | < 6 KB combinado |
| Líneas de Código Boilerplate | Alto (actions, reducers, types) | Mínimo (definición directa) |
| Manejo de Estados de Carga/Error | Manual (isLoading, error en store) |
Automático (isPending, isError) |
| Rendimiento de Renderizado | Suscripciones complejas con reselect | Selectores directos atómicos |
Conclusión#
Separar el estado de UI con Zustand y la sincronización de datos de servidor con TanStack Query es el estándar moderno en la industria para aplicaciones móviles con React Native y Expo de alto rendimiento.
¿Buscas desarrollar una aplicación móvil para iOS y Android con arquitectura limpia, escalable y optimizada? Explora nuestros Servicios de Desarrollo de Apps Móviles o ponte en contacto con nosotros hoy mismo.
¿Te gustaría profundizar en estos temas?
Aprende sobre desarrollo de software, apps a medida, automatizaciones con N8N, Next.js y Cloud con casos reales.


