Gerenciamento de estado avançado: Jotai, Recoil, XState
1. Introdução ao ecossistema de estado moderno no React
Aplicações React modernas frequentemente ultrapassam os limites do useState e useReducer. Quando múltiplos componentes precisam compartilhar estado, quando fluxos assíncronos se entrelaçam ou quando a lógica de negócio exige garantias formais, o gerenciamento de estado precisa evoluir.
O ecossistema atual oferece três abordagens complementares:
- Estado global (Redux, Zustand): ideal para dados compartilhados entre muitas partes da árvore
- Estado atômico (Jotai, Recoil): estado granular com dependências reativas
- Máquinas de estado (XState): fluxos previsíveis com garantias formais
A escolha entre Jotai, Recoil e XState depende da escala da aplicação, complexidade dos fluxos e necessidade de lógica previsível. Vamos explorar cada um com exemplos práticos.
2. Jotai: Estado atômico com simplicidade e performance
Jotai trata o estado como átomos — unidades mínimas e independentes. A API é minimalista e performática por natureza.
import { atom, useAtom } from 'jotai';
// Átomo primitivo
const contadorAtom = atom(0);
// Átomo derivado (leitura)
const contadorDobroAtom = atom((get) => get(contadorAtom) * 2);
// Átomo derivado (leitura + escrita)
const contadorComAcaoAtom = atom(
(get) => get(contadorAtom),
(get, set, incremento) => set(contadorAtom, get(contadorAtom) + incremento)
);
function Contador() {
const [contador, setContador] = useAtom(contadorAtom);
const [dobro] = useAtom(contadorDobroAtom);
return (
<div>
<p>Contador: {contador}</p>
<p>Dobro: {dobro}</p>
<button onClick={() => setContador(contador + 1)}>+1</button>
</div>
);
}
Para dados assíncronos, Jotai integra-se nativamente com React Suspense:
import { atom, useAtom } from 'jotai';
import { loadable } from 'jotai/utils';
const usuarioAtom = atom(async () => {
const resposta = await fetch('/api/usuario');
return resposta.json();
});
const usuarioLoadable = loadable(usuarioAtom);
function Perfil() {
const [usuario] = useAtom(usuarioLoadable);
if (usuario.state === 'loading') return <div>Carregando...</div>;
if (usuario.state === 'hasError') return <div>Erro: {usuario.error.message}</div>;
return <div>Bem-vindo, {usuario.data.nome}!</div>;
}
Padrões avançados incluem persistência com atomWithStorage e átomos de família:
import { atomWithStorage } from 'jotai/utils';
const temaAtom = atomWithStorage('tema', 'claro');
// atomFamily para listas dinâmicas
import { atomFamily } from 'jotai/utils';
const itemAtomFamily = atomFamily((id) => atom({ id, nome: '', preco: 0 }));
3. Recoil: Estado distribuído com grafo de dependências
Recoil oferece uma arquitetura similar à atômica, mas com um grafo de dependências mais explícito e ferramentas avançadas de debugging.
import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue } from 'recoil';
const listaTarefasAtom = atom({
key: 'listaTarefas',
default: [],
});
const tarefasFiltradasSelector = selector({
key: 'tarefasFiltradas',
get: ({ get }) => {
const tarefas = get(listaTarefasAtom);
const filtro = get(filtroAtom);
return tarefas.filter(t => t.status === filtro);
},
});
function App() {
return (
<RecoilRoot>
<ListaTarefas />
</RecoilRoot>
);
}
Seletores podem depender de outros seletores, formando um grafo reativo:
const estatisticasSelector = selector({
key: 'estatisticas',
get: ({ get }) => {
const tarefas = get(tarefasFiltradasSelector);
return {
total: tarefas.length,
concluidas: tarefas.filter(t => t.concluida).length,
};
},
});
O selectorFamily é ideal para cache de requisições:
const dadosUsuarioFamily = selectorFamily({
key: 'dadosUsuario',
get: (userId) => async () => {
const resposta = await fetch(`/api/usuarios/${userId}`);
return resposta.json();
},
});
Comparado ao Jotai, Recoil oferece DevTools mais maduros e uma API mais verbosa, mas com maior controle sobre dependências.
4. XState: Máquinas de estado finito para fluxos previsíveis
XState modela o comportamento da aplicação como máquinas de estado finito, garantindo que apenas transições válidas ocorram.
import { createMachine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';
const maquinaAutenticacao = createMachine({
id: 'autenticacao',
initial: 'deslogado',
states: {
deslogado: {
on: { LOGIN: 'carregando' },
},
carregando: {
invoke: {
src: 'autenticar',
onDone: 'logado',
onError: 'erro',
},
},
logado: {
on: { LOGOUT: 'deslogado' },
},
erro: {
on: { TENTAR_NOVAMENTE: 'carregando' },
},
},
});
function Login() {
const [state, send] = useMachine(maquinaAutenticacao, {
services: {
autenticar: () => fetch('/api/login', { method: 'POST' }).then(r => r.json()),
},
});
return (
<div>
{state.matches('deslogado') && (
<button onClick={() => send('LOGIN')}>Entrar</button>
)}
{state.matches('carregando') && <div>Autenticando...</div>}
{state.matches('logado') && (
<div>
<p>Bem-vindo!</p>
<button onClick={() => send('LOGOUT')}>Sair</button>
</div>
)}
{state.matches('erro') && (
<div>
<p>Erro de autenticação</p>
<button onClick={() => send('TENTAR_NOVAMENTE')}>Tentar novamente</button>
</div>
)}
</div>
);
}
Casos de uso reais incluem formulários multi-etapas, fluxos de checkout e autenticação. A testabilidade é um dos maiores benefícios:
import { interpret } from 'xstate';
test('deve transitar para logado após autenticação bem-sucedida', () => {
const servico = interpret(maquinaAutenticacao.withConfig({
services: { autenticar: () => Promise.resolve({ token: 'abc' }) },
})).start();
servico.send('LOGIN');
expect(servico.state.value).toBe('carregando');
return servico.state.context.promise.then(() => {
expect(servico.state.value).toBe('logado');
});
});
5. Padrões híbridos: combinando abordagens para máxima eficiência
A combinação de abordagens potencializa os pontos fortes de cada biblioteca.
// Jotai para estado do carrinho + XState para fluxo de pagamento
import { atom, useAtom } from 'jotai';
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';
const carrinhoAtom = atom([]);
const totalAtom = atom((get) => {
const itens = get(carrinhoAtom);
return itens.reduce((acc, item) => acc + item.preco * item.quantidade, 0);
});
const maquinaPagamento = createMachine({
id: 'pagamento',
initial: 'revisando',
states: {
revisando: { on: { FINALIZAR: 'processando' } },
processando: {
invoke: {
src: 'processarPagamento',
onDone: 'concluido',
onError: 'falha',
},
},
concluido: { type: 'final' },
falha: { on: { TENTAR_NOVAMENTE: 'processando' } },
},
});
function Checkout() {
const [carrinho] = useAtom(carrinhoAtom);
const [total] = useAtom(totalAtom);
const [state, send] = useMachine(maquinaPagamento);
return (
<div>
<h2>Carrinho ({carrinho.length} itens)</h2>
<p>Total: R$ {total.toFixed(2)}</p>
{state.matches('revisando') && (
<button onClick={() => send('FINALIZAR')}>Finalizar compra</button>
)}
{state.matches('processando') && <p>Processando pagamento...</p>}
{state.matches('concluido') && <p>Pagamento aprovado!</p>}
{state.matches('falha') && (
<div>
<p>Falha no pagamento</p>
<button onClick={() => send('TENTAR_NOVAMENTE')}>Tentar novamente</button>
</div>
)}
</div>
);
}
6. Boas práticas, armadilhas e desempenho
Armadilhas comuns:
- Jotai: átomos excessivamente granulares podem causar muitas re-renderizações. Agrupe estados relacionados em um único átomo.
- Recoil: dependências circulares entre seletores causam erros em runtime. Use
selectorcomgetque não dependa do próprio seletor. - XState: máquinas inchadas com muitos estados. Divida em máquinas menores e use hierarquia de estados.
Estratégias de teste:
// Testando átomos Jotai isoladamente
import { createStore } from 'jotai';
test('átomo derivado calcula corretamente', () => {
const store = createStore();
store.set(contadorAtom, 5);
expect(store.get(contadorDobroAtom)).toBe(10);
});
Performance no SSR:
// Jotai com SSR
import { Provider } from 'jotai';
function App({ initialValues }) {
return (
<Provider initialValues={initialValues}>
<Conteudo />
</Provider>
);
}
// Recoil com hidratação
import { useRecoilCallback } from 'recoil';
function Hydratacao({ initialData }) {
useRecoilCallback(({ set }) => {
set(usuarioAtom, initialData.usuario);
set(temaAtom, initialData.tema);
}, []);
return null;
}
Ferramentas de debugging:
- Jotai:
useAtomDevtoolspara inspecionar átomos - Recoil: DevTools nativos com visualização do grafo de dependências
- XState: XState Inspector para visualizar estados e transições em tempo real
7. Conclusão e guia de decisão
| Cenário | Biblioteca recomendada |
|---|---|
| Estado local com poucas dependências | Jotai |
| Estado global com dependências complexas | Recoil |
| Fluxos determinísticos com garantias formais | XState |
| Combinação de estado simples + fluxo complexo | Jotai + XState |
Tendências futuras: Com React Server Components e React 19 Actions, o gerenciamento de estado tende a se aproximar do servidor. Jotai já oferece suporte experimental a Server Components, e XState pode modelar ações do servidor como transições de máquina.
A escolha final depende do seu contexto: Jotai para simplicidade, Recoil para dependências complexas, XState para fluxos determinísticos. Ou combine-os para máxima eficiência.
Referências
- Documentação oficial do Jotai — Guia completo com exemplos de átomos, derivados e integração com React Suspense
- Documentação oficial do Recoil — Referência da API, incluindo átomos, seletores e selectorFamily
- Documentação oficial do XState — Tutoriais sobre máquinas de estado, interpretação e integração com React
- Jotai vs Recoil: comparação detalhada — Artigo técnico comparando performance, API e casos de uso
- XState: guia prático para formulários multi-etapas — Tutorial passo a passo com exemplos reais de fluxos de formulário
- Gerenciamento de estado no React: guia completo — Artigo abrangente cobrindo desde useState até soluções avançadas como Jotai e XState
- XState Viz: visualizador de máquinas — Ferramenta gráfica para projetar e depurar máquinas de estado interativamente