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.




