¿TanStack Query reemplaza Zustand o Redux?
¿TanStack Query reemplaza Zustand o Redux?
Section titled “¿TanStack Query reemplaza Zustand o Redux?”Una de las preguntas más frecuentes cuando se aprende TanStack Query es: “¿Necesito seguir usando Zustand/Redux/Jotai si ya tengo TanStack Query?” La respuesta corta es: generalmente sí, porque resuelven problemas diferentes.
La Diferencia Fundamental
Section titled “La Diferencia Fundamental”🌐 TanStack Query = Estado Remoto (Server State)
Section titled “🌐 TanStack Query = Estado Remoto (Server State)”TanStack Query está diseñado específicamente para manejar datos que viven en el servidor y necesitan ser sincronizados con tu aplicación.
🏠 Zustand/Redux/Jotai = Estado Local/Global (Client State)
Section titled “🏠 Zustand/Redux/Jotai = Estado Local/Global (Client State)”Estas librerías manejan datos que viven únicamente en tu aplicación y controlan la lógica de negocio del cliente.
Comparación Detallada
Section titled “Comparación Detallada”Aspecto | TanStack Query | Zustand/Redux/Jotai |
---|---|---|
Propósito principal | Sincronizar datos del servidor | Manejar estado local de la app |
Origen de los datos | API/servidor | Creados en el cliente |
Persistencia | Los datos “reales” están en el servidor | Solo en memoria (o localStorage) |
Caché | Automático, con invalidación inteligente | Manual, si se implementa |
Sincronización | Automática entre tabs/ventanas | Manual |
Retry automático | ✅ Sí | ❌ No |
Loading states | ✅ Automático | 🔶 Manual |
Optimistic updates | ✅ Con rollback automático | 🔶 Implementación manual |
Background refresh | ✅ Automático | ❌ No |
Ejemplos Prácticos
Section titled “Ejemplos Prácticos”🌐 Usa TanStack Query para:
Section titled “🌐 Usa TanStack Query para:”// ✅ Datos de usuarios desde una APIconst { data: users } = useQuery({ queryKey: ['users'], queryFn: () => fetch('/api/users').then(res => res.json())});
// ✅ Perfil del usuario actualconst { data: userProfile } = useQuery({ queryKey: ['user', userId], queryFn: () => getUserProfile(userId)});
// ✅ Lista de productos con filtrosconst { data: products } = useQuery({ queryKey: ['products', { category, search }], queryFn: () => fetchProducts({ category, search })});
// ✅ Datos de un dashboardconst { data: analytics } = useQuery({ queryKey: ['analytics', dateRange], queryFn: () => getAnalytics(dateRange), refetchInterval: 30000 // Se actualiza cada 30 segundos});
🏠 Usa Zustand/Redux para:
Section titled “🏠 Usa Zustand/Redux para:”// ✅ Estado de la UI (qué modal está abierto)const useUIStore = create((set) => ({ isModalOpen: false, currentModal: null, openModal: (modalName) => set({ isModalOpen: true, currentModal: modalName }), closeModal: () => set({ isModalOpen: false, currentModal: null })}));
// ✅ Carrito de compras (antes de enviar al servidor)const useCartStore = create((set, get) => ({ items: [], total: 0, addItem: (product) => set((state) => ({ items: [...state.items, product], total: state.total + product.price })), removeItem: (productId) => set((state) => ({ items: state.items.filter(item => item.id !== productId), total: state.items .filter(item => item.id !== productId) .reduce((sum, item) => sum + item.price, 0) }))}));
// ✅ Configuración de la aplicaciónconst useAppStore = create((set) => ({ theme: 'light', language: 'es', sidebarCollapsed: false, toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })), toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed }))}));
// ✅ Estado de formularios complejosconst useFormStore = create((set) => ({ currentStep: 1, formData: {}, errors: {}, nextStep: () => set((state) => ({ currentStep: Math.min(state.currentStep + 1, 5) })), updateField: (field, value) => set((state) => ({ formData: { ...state.formData, [field]: value } }))}));
Casos de Uso Comunes
Section titled “Casos de Uso Comunes”📱 Aplicación de E-commerce
Section titled “📱 Aplicación de E-commerce”// ❌ MAL: Usar Redux para datos del servidorconst productsSlice = createSlice({ name: 'products', initialState: { items: [], loading: false }, reducers: { fetchProductsStart: (state) => { state.loading = true }, fetchProductsSuccess: (state, action) => { state.items = action.payload; state.loading = false; } }});
// ✅ MEJOR: TanStack Query para productos + Zustand para carrito// Productos desde el servidorconst { data: products, isLoading } = useQuery({ queryKey: ['products'], queryFn: fetchProducts});
// Estado local del carritoconst useCartStore = create((set) => ({ items: [], addToCart: (product) => set((state) => ({ items: [...state.items, product] }))}));
function ProductList() { const { addToCart } = useCartStore();
if (isLoading) return <Loading />;
return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} onAddToCart={() => addToCart(product)} /> ))} </div> );}
📊 Dashboard con Datos en Tiempo Real
Section titled “📊 Dashboard con Datos en Tiempo Real”// TanStack Query para datos del servidorconst { data: metrics } = useQuery({ queryKey: ['dashboard-metrics'], queryFn: fetchDashboardMetrics, refetchInterval: 15000 // Actualizar cada 15 segundos});
// Zustand para estado de la UI del dashboardconst useDashboardStore = create((set) => ({ selectedTimeRange: '7d', activeWidgets: ['sales', 'users', 'revenue'], layoutMode: 'grid', setTimeRange: (range) => set({ selectedTimeRange: range }), toggleWidget: (widget) => set((state) => ({ activeWidgets: state.activeWidgets.includes(widget) ? state.activeWidgets.filter(w => w !== widget) : [...state.activeWidgets, widget] }))}));
function Dashboard() { const { selectedTimeRange, activeWidgets } = useDashboardStore();
// Los datos vienen de TanStack Query const { data: salesData } = useQuery({ queryKey: ['sales', selectedTimeRange], queryFn: () => fetchSalesData(selectedTimeRange) });
return ( <div> {activeWidgets.includes('sales') && ( <SalesWidget data={salesData} /> )} </div> );}
¿Cuándo Usar Ambos Juntos?
Section titled “¿Cuándo Usar Ambos Juntos?”🤝 Combinación Perfecta
Section titled “🤝 Combinación Perfecta”La mayoría de aplicaciones reales usan ambos tipos de estado:
function UserProfile() { // 🌐 Datos del servidor (TanStack Query) const { data: user, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) });
// 🏠 Estado local de la UI (Zustand) const { isEditing, startEditing, cancelEditing } = useUIStore();
if (isLoading) return <Loading />;
return ( <div> <h1>{user.name}</h1> <p>{user.email}</p>
{isEditing ? ( <EditUserForm user={user} onCancel={cancelEditing} /> ) : ( <button onClick={startEditing}> Editar Perfil </button> )} </div> );}
📝 Ejemplo: Aplicación de Tareas (TodoApp)
Section titled “📝 Ejemplo: Aplicación de Tareas (TodoApp)”// 🌐 TanStack Query: Tareas desde el servidorconst { data: tasks, isLoading } = useQuery({ queryKey: ['tasks'], queryFn: fetchTasks});
const updateTaskMutation = useMutation({ mutationFn: updateTask, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tasks'] }); }});
// 🏠 Zustand: Estado local de filtros y UIconst useTodoStore = create((set) => ({ filter: 'all', // 'all', 'completed', 'pending' searchQuery: '', sortBy: 'date',
setFilter: (filter) => set({ filter }), setSearch: (query) => set({ searchQuery: query }), setSortBy: (sortBy) => set({ sortBy })}));
function TodoApp() { const { filter, searchQuery, sortBy } = useTodoStore();
// Filtrar y ordenar las tareas (lógica local) const filteredTasks = useMemo(() => { if (!tasks) return [];
return tasks .filter(task => { if (filter === 'completed') return task.completed; if (filter === 'pending') return !task.completed; return true; }) .filter(task => task.title.toLowerCase().includes(searchQuery.toLowerCase()) ) .sort((a, b) => { if (sortBy === 'date') return new Date(b.createdAt) - new Date(a.createdAt); if (sortBy === 'title') return a.title.localeCompare(b.title); return 0; }); }, [tasks, filter, searchQuery, sortBy]);
return ( <div> <TodoFilters /> {/* Usa Zustand para filtros */} <TodoList tasks={filteredTasks} onUpdateTask={updateTaskMutation.mutate} /> </div> );}
Guía de Decisión Rápida
Section titled “Guía de Decisión Rápida”🤔 ¿Qué herramienta usar?
Section titled “🤔 ¿Qué herramienta usar?”Hacete estas preguntas:
-
¿Los datos vienen de una API o servidor?
- ✅ SÍ → TanStack Query
- ❌ NO → Zustand/Redux/Jotai
-
¿Los datos necesitan sincronizarse entre pestañas?
- ✅ SÍ → TanStack Query (automático)
- ❌ NO → Zustand/Redux (para estado local)
-
¿Necesito cache automático y reintentos?
- ✅ SÍ → TanStack Query
- ❌ NO → Zustand/Redux
-
¿Es estado de la interfaz (modales, formularios, tema)?
- ✅ SÍ → Zustand/Redux/Jotai
- ❌ NO → TanStack Query
📋 Checklist Práctico
Section titled “📋 Checklist Práctico”Tipo de Dato | Herramienta Recomendada |
---|---|
👤 Datos de usuario desde API | TanStack Query |
🛒 Items en el carrito (antes de guardar) | Zustand/Redux |
📊 Métricas del dashboard | TanStack Query |
🎨 Tema de la aplicación (dark/light) | Zustand/Redux |
📝 Lista de tareas desde servidor | TanStack Query |
🔍 Filtros de búsqueda actuales | Zustand/Redux |
💬 Mensajes de chat desde API | TanStack Query |
📱 Estado del sidebar (abierto/cerrado) | Zustand/Redux |
Conclusión
Section titled “Conclusión”TanStack Query y las librerías de estado global son complementarias, no competidoras.
- TanStack Query es tu mejor amigo para todo lo que viene del servidor
- Zustand/Redux/Jotai son perfectos para el estado que vive solo en tu app
La combinación de ambos te da lo mejor de los dos mundos: sincronización automática de datos remotos + control total del estado local.
💡 Consejo Final
Section titled “💡 Consejo Final”Empezá con TanStack Query para todos tus datos de servidor. Vas a notar que muchas cosas que antes manejabas con Redux (como loading states, error handling, cache) ya no las necesitás. Después, usá Zustand o Redux solo para el estado local que realmente lo requiera.
¡Tu aplicación va a ser más simple, robusta y fácil de mantener!