blense.
InicioFrontendProgramaciónIANoticiasGuíasCheatsheetsAcerca de

Tecnología descifrada, siempre.

Una lectura curada sobre IA, producto y la cultura detrás del código. Sin ruido, sin hype.

blense.

Tecnología con foco. Historias sobre lo que la innovación realmente cambia.

Secciones

FrontendInteligencia artificialProgramación y desarrolloNoticias tech

Blense

Acerca deRSSPrivacidadTérminosCookies
© 2026 Blense · blense.fun
Frontend

Zustand + TanStack Query: La dupla poderosa para la gestión de estado en React

Descubre por qué Zustand y TanStack Query son la combinación ideal para separar el estado de cliente del servidor y simplificar tu código en React.

Gpor Genildo Souza18 jun11 min de lectura
Zustand + TanStack Query: La dupla poderosa para la gestión de estado en React

TL;DR:Zustand se encarga del estado de cliente (UI, filtros, preferencias). TanStack Query se encarga del estado de servidor (datos de la API, caché, sincronización). Cada uno resuelve exactamente un problema — y ninguno interfiere con el otro. Juntos, reemplazan Redux + lógica de fetch manual con mucho menos código y mucha más robustez.


Dos tipos de estado, dos problemas diferentes

El punto de partida de esta arquitectura es una distinción conceptual que parece obvia después de entenderla, pero que lleva a mucha gente a crear soluciones excesivamente complejas: server state y client state son problemas fundamentalmente diferentes.

Arsenal del dev de élite.
TypeScript con Zustand + TanStack Query: los tipos que los tutoriales no muestran9 minNext.js Firebase: Backend Escalable con TypeScript15 minRuntime: El Concepto Que Nadie Te Explicó Bien (Y Que Lo Cambia Todo Cuando Lo Entiendes)10 minAngular 22: Reemplace ControlValueAccessor con Signal Forms14 min

Server state es cualquier dato que vive fuera de tu aplicación — en una base de datos, en una API. Puede cambiar sin que el usuario haga nada (otro usuario editó el mismo registro, el servidor actualizó un precio). Exige búsqueda asíncrona, caché, revalidación y tratamiento de errores. Gestionarlo con useState + useEffect manual es reinventar la rueda — una rueda cuadrada.

Client state es lo que vive enteramente en el navegador: qué pestaña está activa, qué filtro seleccionó el usuario, si el modal está abierto, el tema de la interfaz. Es síncrono, controlado y no tiene staleness. No necesita caché — solo reactividad.

La propia documentación de TanStack Query es directa: la biblioteca no reemplaza gestores de estado de cliente como Zustand. Ella resuelve el servidor. Zustand resuelve el cliente. Usar ambos juntos es la arquitectura — no una elección entre ellos.

Zustand: Estado de cliente sin ceremonia

Zustand ("estado" en alemán) fue creado con una premisa simple: la gestión de estado global no debería necesitar Providers, reducers, action creators o connect(). Una store es un hook. Fin.

La API minimalista

TYPESCRIPT
import { create } from 'zustand'interface FilterState {  selectedCategory: string  sortOrder: 'asc' | 'desc' | ''  setSelectedCategory: (category: string) => void  setSortOrder: (order: 'asc' | 'desc' | '') => void}// A store inteira em um hook — sem Provider, sem boilerplateexport const useFilterStore = create<FilterState>()((set) => ({  selectedCategory: '',  sortOrder: '',  setSelectedCategory: (selectedCategory) => set({ selectedCategory }),  setSortOrder: (sortOrder) => set({ sortOrder }),}))// Consumindo em qualquer componentefunction SortControl() {  const sortOrder = useFilterStore((s) => s.sortOrder)  const setSortOrder = useFilterStore((s) => s.setSortOrder)  return <select value={sortOrder} onChange={(e) => setSortOrder(e.target.value)} />}

Selectores: el detalle que define el rendimiento

El punto crítico que la mayoría de los tutoriales ignora: Zustand compara el retorno del selector mediante igualdad estricta (Object.is). Esto significa que tres formas de consumir la store tienen impactos de rendimiento radicalmente diferentes.

TYPESCRIPT
// ❌ Assina o store inteiro — re-renderiza em qualquer mudançaconst state = useFilterStore()// ❌ Cria novo objeto a cada render → re-renderização infinitaconst { category } = useFilterStore((s) => ({ category: s.selectedCategory }))// ✅ Seletor atômico — só re-renderiza quando selectedCategory mudaconst category = useFilterStore((s) => s.selectedCategory)// ✅ Múltiplos valores: use useShallow para comparação shallowimport { useShallow } from 'zustand/react/shallow'const { category, sortOrder } = useFilterStore(  useShallow((s) => ({    category: s.selectedCategory,    sortOrder: s.sortOrder,  })))

Regla práctica: siempre exporta custom hooks que encapsulen los selectores, no la store directamente. Esto evita que los componentes se suscriban a toda la store por descuido.

Persist: Preferencias que sobreviven al reload

TYPESCRIPT
import { create } from 'zustand'import { persist, createJSONStorage } from 'zustand/middleware'export const useAppStore = create<AppState>()(  persist(    (set) => ({      theme: 'light' as 'light' | 'dark',      sidebarOpen: false,         // ← não queremos persistir isso      setTheme: (theme) => set({ theme }),      toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),    }),    {      name: 'app-preferences',      storage: createJSONStorage(() => localStorage),      // partialize: persiste só o que faz sentido entre sessões      partialize: (s) => ({ theme: s.theme }),    }  ))// ⚠️ Em Next.js com SSR: proteja o acesso atrás de um flag de hydration// pois localStorage não existe no servidor.

Slices Pattern para dominios separados

Cuando la app crece, divide la store por dominio en slices y combínalos en una bound store. Los middlewares siempre van en la store combinada, nunca en los slices individuales.

TYPESCRIPT
// store/slices/filterSlice.tsexport const createFilterSlice = (set) => ({  selectedCategory: '',  sortOrder: '' as SortOrder,  setSelectedCategory: (selectedCategory: string) => set({ selectedCategory }),  setSortOrder: (sortOrder: SortOrder) => set({ sortOrder }),})// store/slices/uiSlice.tsexport const createUISlice = (set) => ({  theme: 'light' as Theme,  modalOpen: false,  setTheme: (theme: Theme) => set({ theme }),  openModal: () => set({ modalOpen: true }),  closeModal: () => set({ modalOpen: false }),})// store/index.ts — middlewares aqui, nunca nas slicesexport const useBoundStore = create<BoundStore>()(  persist(    devtools((...a) => ({      ...createFilterSlice(...a),      ...createUISlice(...a),    })),    { name: 'app-store', partialize: (s) => ({ theme: s.theme }) }  ))

TanStack Query: Estado de servidor hecho correctamente

TanStack Query es un gestor asíncrono de estado. Cada query es identificada por una queryKey y ejecutada por una queryFn. El caché, la revalidación, los retries automáticos y los estados de loading/error vienen de regalo.

Query Keys jerárquicas e invalidación en cascada

La query key es el índice del caché. El estándar recomendado por la comunidad es el query key factory: una estructura jerárquica tipada que facilita la invalidación en cascada y elimina strings mágicas esparcidas por el código.

TYPESCRIPT
// queries/productKeys.tsexport const productKeys = {  all: ['products'] as const,  lists: () => [...productKeys.all, 'list'] as const,  list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,  details: () => [...productKeys.all, 'detail'] as const,  detail: (id: number) => [...productKeys.details(), id] as const,}// Invalidação hierárquica: invalida tudo que começa com ['products']queryClient.invalidateQueries({ queryKey: productKeys.all })// Invalida só as listas (não os detalhes individuais)queryClient.invalidateQueries({ queryKey: productKeys.lists() })// Invalida só o detalhe de um produto específicoqueryClient.invalidateQueries({ queryKey: productKeys.detail(42) })

Caché, Stale-While-Revalidate y sincronización automática

TanStack Query implementa el patrón stale-while-revalidate: cuando una query está stale, el componente recibe los datos del caché inmediatamente (sin flash de loading) y una petición en background revalida los datos. Los dos parámetros centrales son:

  • staleTime: tiempo en ms antes de que los datos sean considerados obsoletos. Default: 0 (siempre stale). Define valores mayores para datos que cambian poco.

  • gcTime (antiguo cacheTime): cuánto tiempo las entradas inactivas permanecen en memoria antes de que el garbage collector las elimine. Default: 5 minutos.

TYPESCRIPT
// Window focus refetching: quando o usuário volta à aba, dados stale são revalidados// Ativo por default — desative globalmente só se tiver motivo realconst queryClient = new QueryClient({  defaultOptions: {    queries: {      staleTime: 1000 * 60 * 2,       // dados frescos por 2 minutos      refetchOnWindowFocus: true,      // padrão — deixe ativo      refetchOnReconnect: true,        // revalida ao reconectar à internet    },  },})// Polling: refetch periódico, condicional via funçãouseQuery({  queryKey: productKeys.detail(jobId),  queryFn: () => fetchJobStatus(jobId),  refetchInterval: (query) => {    // Para o polling quando o job termina    if (query.state.data?.status === 'complete') return false    return 2_000 // a cada 2s enquanto pendente  },})

Estados de carga en la v5: isPending, isFetching e isLoading

La v5 trajo una distinción importante que rompió código de la v4. Entender los tres estados evita bugs sutiles:

TYPESCRIPT
const { isPending, isFetching, isError, data, error } = useQuery({  queryKey: productKeys.list(filters),  queryFn: () => fetchProducts(filters),})// isPending: query ainda não tem dados em cache (primeiro load)// isFetching: queryFn está executando — inclui background refetches// isLoading: atalho para isPending && isFetching (primeiro load ativo)// Padrão correto em v5:if (isPending) return <Spinner />         // sem dados aindaif (isError) return <ErrorAlert>{error.message}</ErrorAlert>// A partir daqui TypeScript sabe que data está definidoreturn (  <>    {isFetching && <SubtleRefetchIndicator />}  // feedback sutil em background    <ProductGrid products={data} />  </>)

Mutations con Optimistic Updates y Rollback

El patrón completo de optimistic update exige cuatro pasos disciplinados. Saltar cualquiera crea race conditions o estado inconsistente.

TYPESCRIPT
function useUpdateProduct() {  const queryClient = useQueryClient()  return useMutation({    mutationFn: (product: Partial<Product> & { id: number }) =>      api.products.update(product),    onMutate: async (updatedProduct) => {      // 1) Cancelar refetches em voo para evitar race condition      await queryClient.cancelQueries({ queryKey: productKeys.lists() })      // 2) Snapshot do estado anterior (para rollback)      const previousProducts = queryClient.getQueryData(productKeys.list(filters))      // 3) Atualização otimista no cache      queryClient.setQueryData(productKeys.list(filters), (old: Product[]) =>        old.map((p) => (p.id === updatedProduct.id ? { ...p, ...updatedProduct } : p))      )      return { previousProducts }    },    onError: (_err, _vars, context) => {      // 4a) Rollback em caso de erro      queryClient.setQueryData(productKeys.list(filters), context?.previousProducts)    },    onSettled: () => {      // 4b) Sempre revalidar com o servidor — sucesso OU erro      // Use onSettled, não onSuccess, para garantir resync mesmo após rollback      queryClient.invalidateQueries({ queryKey: productKeys.lists() })    },  })}

La dupla en la práctica: integración real

La regla de oro

Nunca copies datos del servidor a Zustand.

Si estás haciendo setQueryData o setState(resultado_da_api) en Zustand, estás reinventando el caché de TanStack Query — mal. Zustand almacena preferencias; TanStack Query almacena respuestas de la API. Estas fronteras no deben cruzarse.

Criterios de decisión

Usa TanStack Query para:

Criterios para usar TanStack Query y Zustand
TanStack Query
Datos API

Gestiona datos REST o GraphQL, listas, detalles y paginación.

Datos stale
Actualización

Maneja datos que pueden quedar obsoletos y mutaciones con invalidación.

Zustand
Estado UI

Almacena filtros, preferencias y estados como modales o sidebars.

Decide rápido con ejemplos claros para cada herramienta

  • Cualquier dato buscado vía API REST o GraphQL

  • Listas, detalles, paginación, infinite scroll

  • Datos que pueden quedar stale porque el servidor puede cambiarlos

  • Mutations (crear, actualizar, borrar) con invalidación automática

Usa Zustand para:

  • Filtros, ordenamiento y parámetros de búsqueda que parametrizan queries

  • Tema, idioma, preferencias de layout

  • Estado de modales, sidebars, toasts globales

  • Carrito de compras antes del checkout (antes de persistir en el servidor)

  • Flags de navegación, steps de wizard multi-step

Heurística rápida: si otro sistema puede cambiar el dato sin que el usuario actúe, es server state. Si solo el propio usuario cambia, es client state.

Ejemplo integrado: Filtro en Zustand, Query en TanStack

Este es el patrón canónico. Los filtros de Zustand entran en la queryKey — cuando el usuario cambia el filtro, TanStack Query automáticamente dispara una nueva petición o sirve desde el caché si ya fue buscado con esa combinación.

TYPESCRIPT
// 1) store/useFilterStore.ts — Zustand guarda as preferências do usuárioexport const useFilterStore = create<FilterState>()(  persist(    (set) => ({      selectedCategory: '',      sortOrder: '' as SortOrder,      setSelectedCategory: (selectedCategory) => set({ selectedCategory }),      setSortOrder: (sortOrder) => set({ sortOrder }),    }),    { name: 'product-filters' } // preferências persistem entre sessões  ))// 2) hooks/useProducts.ts — bridge entre Zustand e TanStack Queryexport function useProducts() {  // useShallow evita re-render quando outros campos do store mudam  const { selectedCategory, sortOrder } = useFilterStore(    useShallow((s) => ({      selectedCategory: s.selectedCategory,      sortOrder: s.sortOrder,    }))  )  return useQuery({    // Os filtros fazem parte da key — cache por combinação de filtros    queryKey: productKeys.list({ category: selectedCategory, sort: sortOrder }),    queryFn: () => fetchProducts({ category: selectedCategory, sort: sortOrder }),    staleTime: 1000 * 60 * 2,    // Mantém dados anteriores visíveis enquanto os novos carregam    placeholderData: (prev) => prev,  })}// 3) ProductList.tsx — componente que consome ambas as libs via hooksfunction ProductList() {  const { data, isPending, isFetching, isError, error } = useProducts()  const setCategory = useFilterStore((s) => s.setSelectedCategory)  if (isPending) return <Spinner />  if (isError) return <ErrorAlert>{error.message}</ErrorAlert>  return (    <>      <CategorySelector onChange={setCategory} />      {isFetching && <SubtleRefetchIndicator />}      <ProductGrid products={data} />    </>  )}

El componente no sabe que hay dos bibliotecas involucradas. Solo consume hooks. Esa es la capa correcta de abstracción.

Zustand vs Redux: La comparación directa

En 2025–2026, Zustand superó a Redux Toolkit en descargas semanales en proyectos nuevos. La migración tiene sentido cuando adoptas TanStack Query para server state — porque lo que sobra de state global es demasiado pequeño para justificar el overhead de Redux.

  • Bundle: Zustand ~1 kB gzip vs Redux Toolkit ~12–14 kB (sin RTK Query)

  • Boilerplate: Zustand son hooks directos vs slices + actions + reducers + Provider en Redux

  • Curva: Zustand tiene un concepto central (create) vs actions/reducers/selectors/middleware/thunks de Redux

  • DevTools: ambos integran con Redux DevTools vía middleware devtools

  • Cuándo Redux aún gana: equipos grandes con mandato de arquitectura Flux estricta, bases de código legadas, o cuando RTK Query ya está en uso y no compensa migrar

Estructura de proyecto recomendada

TEXT
src/  store/    index.ts                # useBoundStore com todos os slices    slices/      filterSlice.ts        # filtros de UI      uiSlice.ts            # tema, modais, sidebar  queries/    productKeys.ts          # query key factory    useProducts.ts          # useQuery + filtros do Zustand    useProductMutations.ts  # useMutation com optimistic update  components/    ProductList.tsx         # consome useProducts()    FilterPanel.tsx         # consome useFilterStore()  lib/    queryClient.ts          # QueryClient com defaultOptions

Conclusión

La arquitectura Zustand + TanStack Query resuelve el problema histórico de la gestión de estado en React con claridad conceptual: cada biblioteca hace exactamente una cosa y la hace bien. Dejas de tener un Redux gigante que intenta encargarse de todo y pasas a tener dos especialistas que no interfieren entre sí.

Arquitectura Zustand + TanStack Query
  • 1Cada biblioteca tiene un rol claro y especializado, evitando la complejidad de un Redux monolítico.
  • 2Zustand maneja el estado global de UI con un bundle ligero (~1 kB) y una API sencilla.
  • 3TanStack Query automatiza el fetch de datos, eliminando la lógica manual de carga y errores.
  • 4La caché automática con deduplicación optimiza las solicitudes de datos.
  • 5La revalidación en segundo plano ocurre sin necesidad de código adicional.
  • 6Ambas bibliotecas ofrecen soporte completo para TypeScript, mejorando la robustez.

El resultado práctico:

  • Sin lógica de fetch manual (useEffect + useState + loading + error + abort)

  • Caché automático con deduplicación de requests

  • Revalidación inteligente en background sin código adicional

  • Estado de UI global con bundle de ~1 kB y API de 10 líneas

  • TypeScript first-class en ambas bibliotecas

La regla que no cambia: nunca copies datos del servidor a Zustand. Server state vive en TanStack Query. Client state vive en Zustand. Los componentes solo consumen vía hooks. Esa separación es la arquitectura.

Sigue explorando
¿Quieres más contenido así?

Mira otros artículos en la misma línea y mantén el ritmo.

Ver más artículos
#JavaScript#TypeScript#React

Arsenal del dev de élite.

Guía de estrategias de carga HTML que acelerará sus tiempos de carga
Frontend

Guía de estrategias de carga HTML que acelerará sus tiempos de carga

Descubre cómo optimizar la carga de scripts con defer y async para eliminar bloqueos de renderizado y mejorar la velocidad de tu sitio web.

Genildo Souza · 24 jun · 9 min
Tailwind CSS v4: Cómo acelerar tus builds 100x con la nueva configuración CSS-first
Frontend

Tailwind CSS v4: Cómo acelerar tus builds 100x con la nueva configuración CSS-first

Descubre cómo Tailwind CSS v4 revoluciona el desarrollo frontend con builds ultrarrápidos y una nueva configuración CSS-first sin complicaciones.

Genildo Souza · 20 jun · 16 min
El fin de la transición: La estructura de diseño y desarrollo de productos para 2026
Frontend

El fin de la transición: La estructura de diseño y desarrollo de productos para 2026

Durante décadas, crear un producto fue una carrera de relevos ineficiente. En 2026, la IA y los nuevos estándares están eliminando los traspasos.

Genildo Souza · 18 jun · 9 min
En este artículo
  • Dos tipos de estado, dos problemas diferentes
  • Zustand: Estado de cliente sin ceremonia
  • La API minimalista
  • Selectores: el detalle que define el rendimiento
  • Persist: Preferencias que sobreviven al reload
  • Slices Pattern para dominios separados
  • TanStack Query: Estado de servidor hecho correctamente
  • Query Keys jerárquicas e invalidación en cascada
  • Caché, Stale-While-Revalidate y sincronización automática
  • Estados de carga en la v5: isPending, isFetching e isLoading
  • Mutations con Optimistic Updates y Rollback
  • La dupla en la práctica: integración real
  • La regla de oro
  • Criterios de decisión
  • Ejemplo integrado: Filtro en Zustand, Query en TanStack
  • Zustand vs Redux: La comparación directa
  • Estructura de proyecto recomendada
  • Conclusión