Front End

25 mar, 2026

Gestão de estado no front-end: o caos moderno; e como sair dele

Publicidade

Se tem um tema que divide front-end sênior, é esse. Estado. Todo projeto começa simples:

  • um useState aqui
  • um contexto ali
  • um fetch direto no componente

E quando você percebe…

👉 tem estado duplicado
👉 dados inconsistentes
👉 bugs que só aparecem em produção

Bem-vindo ao caos moderno.

O erro raiz: tratar tudo como o mesmo tipo de estado

Esse é o ponto que quase ninguém explica direito.

Nem todo estado é igual.

E misturar tudo é o começo do problema.

Server State vs Client State

Essa distinção muda completamente sua arquitetura.

Server State

Dados que vêm do backend:

  • lista de usuários
  • pedidos
  • dados de API

Características:

  • assíncrono
  • pode ficar desatualizado
  • precisa de cache
  • pode ser compartilhado entre telas

Client State

Estado local da aplicação:

  • modal aberto/fechado
  • tema
  • inputs de formulário

Características:

  • síncrono
  • controlado pela UI
  • não precisa de persistência remota

O erro clássico

function Users() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);

  return <UserList users={users} />;
}

Parece simples.

Mas agora você precisa:

  • refetch automático
  • cache
  • loading global
  • erro
  • sincronização entre telas

👉 Esse código não escala.

Server State do jeito certo

Aqui entram libs como React Query.

Exemplo real com React Query

// api/users.ts
export async function fetchUsers() {
  const res = await fetch('/api/users');

  if (!res.ok) {
    throw new Error('Erro ao buscar usuários');
  }

  return res.json();
}
// hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import { fetchUsers } from '../api/users';

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 1000 * 60, // 1 min
  });
}
// Users.tsx
export function Users() {
  const { data, isLoading, error } = useUsers();

  if (isLoading) return <p>Carregando...</p>;
  if (error) return <p>Erro ao carregar</p>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

O que você ganhou aqui

  • cache automático
  • revalidação
  • controle de loading e erro
  • sincronização entre componentes

👉 Isso é nível produção.

Quando NÃO usar Redux

Vamos falar a real.

Redux foi essencial por muito tempo.

Mas hoje…

👉 na maioria dos casos, é overkill.

Exemplo clássico de exagero

// store/usersSlice.ts
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    data: [],
    loading: false,
  },
  reducers: {
    setUsers(state, action) {
      state.data = action.payload;
    },
  },
});

Tudo isso… pra armazenar dado de API.

👉 que já deveria estar no server state.

Quando Redux ainda faz sentido

  • regras de negócio complexas e compartilhadas
  • fluxos altamente previsíveis
  • necessidade forte de rastreabilidade

Se não for isso…

👉 provavelmente você não precisa.

Estado global leve: alternativas modernas

Hoje o jogo mudou.

Você não precisa de um canhão pra matar uma formiga.

Zustand: simples e direto

Zustand é provavelmente o mais pragmático hoje.

Exemplo completo

// store/useCartStore.ts
import { create } from 'zustand';

type CartItem = {
  id: number;
  name: string;
  price: number;
};

type CartStore = {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: number) => void;
};

export const useCartStore = create<CartStore>((set) => ({
  items: [],

  addItem: (item) =>
    set((state) => ({
      items: [...state.items, item],
    })),

  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter(item => item.id !== id),
    })),
}));
// Cart.tsx
import { useCartStore } from './store/useCartStore';

export function Cart() {
  const { items, removeItem } = useCartStore();

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.name}
          <button onClick={() => removeItem(item.id)}>
            Remover
          </button>
        </div>
      ))}
    </div>
  );
}

Jotai: estado atômico

Jotai trabalha com “átomos” de estado.

Exemplo

// atoms/cartAtom.ts
import { atom } from 'jotai';

export const cartAtom = atom([]);
// Cart.tsx
import { useAtom } from 'jotai';
import { cartAtom } from './atoms/cartAtom';

export function Cart() {
  const [cart, setCart] = useAtom(cartAtom);

  function addItem(item) {
    setCart(prev => [...prev, item]);
  }

  return (
    <div>
      <button onClick={() => addItem({ id: 1, name: 'Item' })}>
        Adicionar
      </button>

      {cart.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

Signals: o futuro (talvez)

Signals estão ganhando espaço.

A ideia:

👉 reatividade mais granular
👉 menos re-render

Exemplo conceitual

import { signal } from '@preact/signals-react';

const count = signal(0);

export function Counter() {
  return (
    <div>
      <p>{count.value}</p>
      <button onClick={() => count.value++}>
        Incrementar
      </button>
    </div>
  );
}

Cache e sincronização: onde tudo quebra

Esse é o verdadeiro desafio.

Problema real

Você tem:

  • lista de usuários
  • tela de edição
  • outra tela usando os mesmos dados

Se você não sincroniza:

👉 dados ficam inconsistentes
👉 UI mostra informação antiga

Exemplo com invalidação de cache

import { useMutation, useQueryClient } from '@tanstack/react-query';

function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (user) => {
      await fetch(`/api/users/${user.id}`, {
        method: 'PUT',
        body: JSON.stringify(user),
      });
    },
    onSuccess: () => {
      queryClient.invalidateQueries(['users']);
    },
  });
}

Resultado

  • lista atualiza automaticamente
  • estado consistente
  • menos bugs

O padrão que funciona em escala

Se você quiser algo sólido:

  • Server state → React Query
  • Client state simples → useState / context
  • Global leve → Zustand
  • Evitar Redux sem necessidade

O erro mais comum em times

Não é escolher ferramenta errada.

É misturar tudo.

  • fetch no componente
  • estado global pra tudo
  • duplicação de dados

👉 Resultado: impossível de debugar

Conclusão: menos ferramenta, mais estratégia

Gestão de estado não é sobre biblioteca.

É sobre decisão.

Se você separar bem:

  • o que vem do servidor
  • o que pertence à UI

Você já resolveu 80% do problema.

O resto é ferramenta.

No fim…

👉 júnior escolhe biblioteca
👉 sênior escolhe modelo mental

E isso muda tudo.