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 selector com get que 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: useAtomDevtools para 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