Skip to content

¿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.

🌐 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.

AspectoTanStack QueryZustand/Redux/Jotai
Propósito principalSincronizar datos del servidorManejar estado local de la app
Origen de los datosAPI/servidorCreados en el cliente
PersistenciaLos datos “reales” están en el servidorSolo en memoria (o localStorage)
CachéAutomático, con invalidación inteligenteManual, si se implementa
SincronizaciónAutomática entre tabs/ventanasManual
Retry automático✅ Sí❌ No
Loading states✅ Automático🔶 Manual
Optimistic updates✅ Con rollback automático🔶 Implementación manual
Background refresh✅ Automático❌ No
// ✅ Datos de usuarios desde una API
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json())
});
// ✅ Perfil del usuario actual
const { data: userProfile } = useQuery({
queryKey: ['user', userId],
queryFn: () => getUserProfile(userId)
});
// ✅ Lista de productos con filtros
const { data: products } = useQuery({
queryKey: ['products', { category, search }],
queryFn: () => fetchProducts({ category, search })
});
// ✅ Datos de un dashboard
const { data: analytics } = useQuery({
queryKey: ['analytics', dateRange],
queryFn: () => getAnalytics(dateRange),
refetchInterval: 30000 // Se actualiza cada 30 segundos
});
// ✅ 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ón
const 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 complejos
const 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 }
}))
}));
// ❌ MAL: Usar Redux para datos del servidor
const 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 servidor
const { data: products, isLoading } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
// Estado local del carrito
const 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>
);
}
// TanStack Query para datos del servidor
const { data: metrics } = useQuery({
queryKey: ['dashboard-metrics'],
queryFn: fetchDashboardMetrics,
refetchInterval: 15000 // Actualizar cada 15 segundos
});
// Zustand para estado de la UI del dashboard
const 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>
);
}

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 servidor
const { data: tasks, isLoading } = useQuery({
queryKey: ['tasks'],
queryFn: fetchTasks
});
const updateTaskMutation = useMutation({
mutationFn: updateTask,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
}
});
// 🏠 Zustand: Estado local de filtros y UI
const 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>
);
}

Hacete estas preguntas:

  1. ¿Los datos vienen de una API o servidor?

    • → TanStack Query
    • NO → Zustand/Redux/Jotai
  2. ¿Los datos necesitan sincronizarse entre pestañas?

    • → TanStack Query (automático)
    • NO → Zustand/Redux (para estado local)
  3. ¿Necesito cache automático y reintentos?

    • → TanStack Query
    • NO → Zustand/Redux
  4. ¿Es estado de la interfaz (modales, formularios, tema)?

    • → Zustand/Redux/Jotai
    • NO → TanStack Query
Tipo de DatoHerramienta Recomendada
👤 Datos de usuario desde APITanStack Query
🛒 Items en el carrito (antes de guardar)Zustand/Redux
📊 Métricas del dashboardTanStack Query
🎨 Tema de la aplicación (dark/light)Zustand/Redux
📝 Lista de tareas desde servidorTanStack Query
🔍 Filtros de búsqueda actualesZustand/Redux
💬 Mensajes de chat desde APITanStack Query
📱 Estado del sidebar (abierto/cerrado)Zustand/Redux

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.

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!