blense.
InícioFrontendProgramaçãoIANotíciasGuiasCheatsheetsSobre

Tecnologia decodificada, sempre.

Uma leitura curada sobre IA, produto e a cultura por trás do código. Sem ruído, sem hype.

blense.

Tecnologia com foco. Histórias sobre o que a inovação realmente muda.

Seções

FrontendInteligência ArtificialProgramação & DevNotícias Tech

Blense

SobreRSSPolítica de privacidadeTermosCookies
© 2026 Blense · blense.fun
Frontend

Estado de Cliente e Servidor: Use Zustand e TanStack Query

O Redux não foi construído pra guardar a resposta da sua API. Foi construído pra gerenciar estado de interface. Em algum momento, esse limite desapareceu — e uma camada de boilerplate surgiu pra tapar o buraco que não deveria existir.

Gpor Genildo Souza28 de mar.7 min de leitura
Estado de Cliente e Servidor: Use Zustand e TanStack Query

Dois problemas que pareciam um

Estado em uma aplicação React tem duas formas radicalmente diferentes. Misturá-las no mesmo lugar é a origem de boa parte da complexidade que você carrega em todo projeto.

Arsenal do dev de elite.
TypeScript com Zustand + TanStack Query: os tipos que os tutoriais não mostram8 minNext.js 15 + React 19: Guia de Implementação Full-Stack11 minMicrofrontends com Angular e Nx: o que ninguém conta16 minNext.js 16 PPR: Elimine o tradeoff entre estático e dinâmico e alcance Edge TTFB com dados atualizados15 min
Estado de servidor

Vive em um servidor remoto

  • Lista de produtos da API

  • Perfil do usuário logado

  • Histórico de pedidos, resultados de busca

  • Precisa de cache, refetch, deduplicação

  • Fica desatualizado. Falha. Precisa de retry.

Estado de cliente

Vive no browser

  • O sidebar está aberto ou fechado?

  • Qual aba está ativa?

  • O que o usuário digitou no filtro?

  • Tema, visibilidade do modal, step do wizard

  • Não precisa de cache nem de refetch

Quando os dois vão pro mesmo Redux slice — ou pra um Context gigante — você acaba escrevendo loading reducers, error reducers, lógica de cache e mecanismos de refetch na mão. Toda essa infraestrutura já existe em bibliotecas construídas especificamente pra estado de servidor.

A solução não foi trocar o Redux por algo melhor. Foi parar de pedir pro Redux resolver um problema que não é dele.

Zustand cuida do estado de cliente. TanStack Query cuida do estado de servidor. Cada um resolve exatamente um problema — e os dois não interferem um no outro.

Zustand: estado global sem cerimônia

Zustand parte de uma premissa: uma store é só um hook. Sem providers envolvendo o app inteiro. Sem actions, reducers ou dispatch. Sem boilerplate que existe apenas porque o padrão exige.

Estado e funções de atualização vivem juntos na mesma store. Componentes se inscrevem em fatias específicas e só re-renderizam quando aquela fatia muda.

JAVASCRIPT
// store/ui.tsimport { create } from 'zustand'interface UIState {  sidebarAberto: boolean  tema: 'claro' | 'escuro'  alternarSidebar: () => void  setTema: (tema: 'claro' | 'escuro') => void}export const useUIStore = create<UIState>()((set) => ({  sidebarAberto: false,  tema: 'claro',  alternarSidebar: () =>    set((state) => ({ sidebarAberto: !state.sidebarAberto })),  setTema: (tema) => set({ tema }),}))

Usando no componente — sem Provider, sem dispatch:

JAVASCRIPT
function Sidebar() {  // subscrição seletiva: só re-renderiza quando sidebarAberto muda  const sidebarAberto = useUIStore(s => s.sidebarAberto)  const alternarSidebar = useUIStore(s => s.alternarSidebar)  return (    <aside className={sidebarAberto ? 'aberto' : 'fechado'}>      <button onClick={alternarSidebar}>Menu</button>    </aside>  )}
ℹ️

Subscrição seletiva importa. Se você desestruturar a store inteira com useUIStore(), o componente re-renderiza em toda mudança de estado — mesmo as que não afetam ele. Prefira useUIStore(s => s.sidebarAberto) pra subscrever só ao que o componente usa.

TanStack Query: estado de servidor que se gerencia sozinho

TanStack Query trata dados de API como um problema de cache, não de estado. Cada chamada recebe uma chave de cache. A biblioteca decide quando buscar, quando servir do cache, quando refazer a requisição em background — e o que mostrar enquanto os dados estão carregando ou desatualizados.

JAVASCRIPT
// hooks/useProdutos.tsimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'export function useProdutos() {  return useQuery({    queryKey: ['produtos'],    queryFn: () =>      fetch('/api/produtos').then(r => r.json()),    staleTime: 1000 * 60 * 5, // 5 min antes do refetch em background  })}export function useCriarProduto() {  const queryClient = useQueryClient()  return useMutation({    mutationFn: (produto: NovoProduto) =>      fetch('/api/produtos', {        method: 'POST',        body: JSON.stringify(produto),      }).then(r => r.json()),    // invalida o cache após a mutação — dispara novo fetch    onSuccess: () => {      queryClient.invalidateQueries({ queryKey: ['produtos'] })    },  })}

O componente que consome fica limpo — sem gerenciar loading, error ou cache manualmente:

JAVASCRIPT
function ListaProdutos() {  const { data: produtos, isPending, isError } = useProdutos()  const criarProduto = useCriarProduto()  if (isPending) return <Spinner />  if (isError)   return <MensagemDeErro />  return (    <ul>      {produtos.map(p => <ItemProduto key={p.id} produto={p} />)}    </ul>  )}

O antes e o depois que mudou como eu penso em estado

Mesma feature — lista de produtos com filtro — usando Redux versus a combinação Zustand + TanStack Query.

✕ Abordagem Redux
JAVASCRIPT
// slice/produtos.tsconst produtosSlice = createSlice({  name: 'produtos',  initialState: {    itens: [],    loading: false,    error: null,    filtro: '',  },  reducers: {    setFiltro: (state, action) => {      state.filtro = action.payload    },  },  extraReducers: (builder) => {    builder      .addCase(buscarProdutos.pending,        (s) => { s.loading = true })      .addCase(buscarProdutos.fulfilled,        (s, a) => {          s.loading = false          s.itens = a.payload        })      .addCase(buscarProdutos.rejected,        (s, a) => {          s.loading = false          s.error = a.error.message        })  }})// thunk/produtos.tsexport const buscarProdutos =  createAsyncThunk(    'produtos/buscar',    async () => {      const res = await fetch('/api/produtos')      return res.json()    }  )// componentefunction ListaProdutos() {  const dispatch = useDispatch()  const { itens, loading,          error, filtro } =    useSelector(s => s.produtos)  useEffect(() => {    dispatch(buscarProdutos())  }, [])  if (loading) return <Spinner />  if (error)   return <Erro />  return (    <>      <input value={filtro}        onChange={e => dispatch(          setFiltro(e.target.value)        )}      />      {itens        .filter(p =>          p.nome.includes(filtro))        .map(p => ...)}    </>  )}
✓ Zustand + TanStack Query
JAVASCRIPT
// store/filtro.tsexport const useFiltroStore =  create()((set) => ({    filtro: '',    setFiltro: (f) =>      set({ filtro: f }),  }))// hooks/useProdutos.tsexport function useProdutos() {  return useQuery({    queryKey: ['produtos'],    queryFn: () =>      fetch('/api/produtos')        .then(r => r.json()),  })}// componentefunction ListaProdutos() {  const { data, isPending,          isError } = useProdutos()  const { filtro, setFiltro } =    useFiltroStore()  if (isPending) return <Spinner />  if (isError)   return <Erro />  return (    <>      <input value={filtro}        onChange={e =>          setFiltro(e.target.value)}      />      {data        .filter(p =>          p.nome.includes(filtro))        .map(p => ...)}    </>  )}

A versão Redux tem mais linhas. Mas o custo real não é de linhas — é de rastreabilidade. Pra entender o que o componente faz, você percorre actions, thunks, reducers e selectors antes de chegar ao render. Na versão com Zustand e TanStack Query, cada peça está onde o problema mora: busca de dados no hook de query, estado de UI na store, renderização no componente.

Quando os dois trabalham juntos: filtro que dispara refetch

O ponto forte aparece quando o estado do Zustand influencia as queries do TanStack Query. Um filtro na store vira dependência da chave de cache — o TanStack Query refaz a requisição automaticamente quando o valor muda.

JAVASCRIPT
// A chave inclui o filtro do Zustand// Quando filtro muda → refetch automáticoexport function useProdutosFiltrados() {  const filtro = useFiltroStore(s => s.filtro)  return useQuery({    queryKey: ['produtos', filtro],    queryFn: () =>      fetch(`/api/produtos?q=${filtro}`).then(r => r.json()),    // não busca com menos de 2 caracteres    enabled: filtro.length > 2,  })}// Query dependente: só busca os pedidos// depois que o usuário estiver disponívelexport function useDetalhesPedido(pedidoId: string) {  const { data: usuario } = useUsuario()  return useQuery({    queryKey: ['pedidos', pedidoId],    queryFn: () => buscarPedido(pedidoId, usuario!.token),    enabled: !!usuario, // só executa quando usuario existe  })}

Update otimista sem saga nem middleware

Atualizar a UI antes da resposta do servidor — update otimista — exigia sagas, thunks customizados ou middleware. No TanStack Query é configuração.

JAVASCRIPT
const alternarFavorito = useMutation({  mutationFn: (produtoId: string) =>    fetch(`/api/favoritos/${produtoId}`, { method: 'POST' }),  // atualiza o cache imediatamente antes da requisição terminar  onMutate: async (produtoId) => {    await queryClient.cancelQueries({ queryKey: ['produtos'] })    const anterior = queryClient.getQueryData(['produtos'])    queryClient.setQueryData(['produtos'], (old: Produto[]) =>      old.map(p =>        p.id === produtoId          ? { ...p, favorito: !p.favorito }          : p      )    )    return { anterior } // snapshot pra rollback  },  // se falhar, volta ao estado anterior  onError: (err, produtoId, context) => {    queryClient.setQueryData(['produtos'], context?.anterior)  },})

Quando essa combinação não faz sentido

  • Estado local resolve? UseuseState. Não adicione Zustand pra estado que não precisa ser compartilhado entre componentes.

  • App pequeno, pouca interação com API? Zustand sozinho, sem TanStack Query, é suficiente.

  • Next.js com Server Components pesados? TanStack Query compete com RSC na busca de dados. Em apps muito orientados ao servidor, talvez não precise dele — ou use só pra mutações no cliente.

  • GraphQL? Apollo Client ou urql já gerenciam cache e estado de servidor. Adicionar TanStack Query cria redundância.

⚠️

O custo real dessa arquitetura é em times que não internalizaram a distinção entre estado de servidor e estado de cliente. Quando isso não está claro, respostas de API acabam no Zustand — e você volta ao problema original com uma nova camada de complexidade por cima.

O que fica depois de ler isso
  • 1Estado de servidor precisa de cache, refetch e retry. TanStack Query entrega tudo isso.
  • 2Estado de cliente é UI — sidebar, filtros, tema. Zustand gerencia com uma store que é um hook.
  • 3Subscrição seletiva no Zustand — use store(s => s.campo) em vez de destruturar tudo.
  • 4Query key com estado do Zustand — filtros na store viram dependências do cache, com refetch automático.
  • 5Update otimista — onMutate atualiza o cache imediatamente, onError faz rollback se necessário.
  • 6Não é pra todo projeto — apps pequenos, GraphQL ou Next.js com RSC têm soluções mais adequadas.

O boilerplate do Redux não era um problema de verbosidade. Era um sintoma de estar usando a ferramenta certa no lugar errado.

Estado de servidor tem ciclo de vida próprio — ele fica desatualizado, falha, precisa de retry, precisa de cache. Tratar isso como estado de UI significa reimplementar, na mão, o que o TanStack Query já resolve.

Quando cada problema tem a ferramenta certa, o código que sobra é só o que importa.

Quer aprofundar este assunto?
Explorar

#JavaScript#TypeScript#React

Arsenal do dev de elite.

Next.js 15 + React 19: Guia de Implementação Full-Stack
Frontend

Next.js 15 + React 19: Guia de Implementação Full-Stack

Aprenda a implementar padrões de alta performance com Next.js 15 e React 19, reduzindo o bundle e otimizando a comunicação entre cliente e servidor.

Genildo Souza · 19 de jun. · 11 min
Como unificar design e código: a estrutura exata para acelerar suas entregas em 2026.
Frontend

Como unificar design e código: a estrutura exata para acelerar suas entregas em 2026.

Chega de perder tempo entre equipes. Aprenda a estrutura de 2026 para unificar design e código e construir produtos digitais sem atritos.

Genildo Souza · 18 de jun. · 9 min
Lazy Loading: carregar menos para entregar mais
Frontend

Lazy Loading: carregar menos para entregar mais

A web nunca foi tão rápida, e nunca engoliu tantos megabytes. A saída não é acelerar a entrega, mas adiar o que não importa agora.

Genildo Souza · 12 de jun. · 13 min
Neste artigo
  • Dois problemas que pareciam um
  • Zustand: estado global sem cerimônia
  • TanStack Query: estado de servidor que se gerencia sozinho
  • O antes e o depois que mudou como eu penso em estado
  • Quando os dois trabalham juntos: filtro que dispara refetch
  • Update otimista sem saga nem middleware
  • Quando essa combinação não faz sentido