Entendendo Jotai: Uma Alternativa Elegante ao Redux
Jotai é uma biblioteca de gerenciamento de estado minimalista para React, desenvolvida com a filosofia de ser leve e intuitiva. Diferentemente de alternativas como Redux ou Zustand que trabalham com uma store centralizada, Jotai adota um modelo descentralizado baseado em atoms — pequenas unidades de estado que podem ser compostas e derivadas. Essa abordagem torna o código mais modular, porque cada atom é uma unidade independente que pode ser reutilizada em diferentes partes da aplicação sem acoplamento desnecessário.
A beleza do Jotai está na sua simplicidade: você define o estado de forma granular e deixa a biblioteca se encarregar de otimizações de renderização. React Components apenas renderizam quando os atoms que eles realmente usam mudam, evitando re-renders desnecessários de forma automática. Essa é uma vantagem significativa em comparação com Context API ou Redux, onde você frequentemente sofre com re-renders em cascata.
Atoms: Construindo Blocos de Estado
O Conceito Fundamental de um Atom
Um atom em Jotai é simplesmente um objeto que representa uma unidade de estado. Você cria atoms usando a função atom() e passa um valor inicial como argumento. Quando um atom muda, qualquer componente que o consome é notificado e renderizado novamente — mas apenas esse componente, graças ao sistema de dependências inteligente do Jotai.
import { atom } from 'jotai';
// Um atom simples com valor primitivo
const countAtom = atom(0);
// Um atom com um objeto
const userAtom = atom({
name: 'João',
email: 'joao@example.com',
age: 28
});
// Um atom com um array
const todoAtom = atom([
{ id: 1, text: 'Aprender Jotai', completed: false },
{ id: 2, text: 'Usar em produção', completed: false }
]);
Lendo e Escrevendo em Atoms
Para interagir com atoms em componentes React, você usa o hook useAtom(). Esse hook retorna um array com dois elementos: o valor atual do atom e uma função para atualizar esse valor. É similar ao useState(), mas o estado é compartilhado globalmente entre todos os componentes que usam o mesmo atom.
import { useAtom } from 'jotai';
import { countAtom } from './atoms';
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Contagem: {count}</p>
<button onClick={() => setCount(count + 1)}>Incrementar</button>
<button onClick={() => setCount(c => c - 1)}>Decrementar</button>
</div>
);
}
Note que a função de atualização pode receber tanto um novo valor direto quanto uma função que recebe o valor anterior — exatamente como em useState(). Isso permite padrões funcionais elegantes quando você precisa fazer cálculos baseados no estado anterior.
Operações Avançadas com Atoms
Às vezes você quer atualizar múltiplos atoms de forma coordenada, ou executar efeitos colaterais quando um atom muda. O Jotai oferece a função useAtomCallback() para esses casos mais complexos.
import { atom, useAtomCallback } from 'jotai';
const firstNameAtom = atom('João');
const lastNameAtom = atom('Silva');
const fullNameAtom = atom('João Silva');
function ProfileUpdater() {
const updateProfile = useAtomCallback(
(get, set) => async (first, last) => {
// get() lê o valor atual de um atom
// set() escreve em um atom
set(firstNameAtom, first);
set(lastNameAtom, last);
set(fullNameAtom, `${first} ${last}`);
// Você pode fazer requisições, validações, etc.
await fetch('/api/profile', {
method: 'POST',
body: JSON.stringify({ first, last })
});
}
);
return (
<button onClick={() => updateProfile('Carlos', 'Santos')}>
Atualizar Perfil
</button>
);
}
Derived State: Computando Novos Estados a Partir de Atoms Existentes
Atoms Derivados com atom()
Um dos conceitos mais poderosos do Jotai é a capacidade de criar atoms derivados — atoms que dependem de outros atoms e são calculados automaticamente. Quando você passa uma função de leitura como argumento para atom(), está criando um atom que computa seu valor baseado em outros atoms.
import { atom } from 'jotai';
const priceAtom = atom(100);
const quantityAtom = atom(5);
// Atom derivado que calcula o total
const totalAtom = atom(
(get) => get(priceAtom) * get(quantityAtom)
);
function ShoppingCart() {
const [price] = useAtom(priceAtom);
const [quantity] = useAtom(quantityAtom);
const [total] = useAtom(totalAtom);
return (
<div>
<p>Preço: R$ {price}</p>
<p>Quantidade: {quantity}</p>
<p style={{ fontWeight: 'bold' }}>Total: R$ {total}</p>
</div>
);
}
A chave aqui é que totalAtom não armazena seu próprio estado. Ele é read-only por padrão — apenas calcula seu valor sob demanda. Quando priceAtom ou quantityAtom mudam, o Jotai automaticamente recalcula totalAtom e notifica apenas os componentes que o usam.
Atoms Derivados com Lógica de Escrita
Às vezes você quer um atom derivado que também possa ser escrito. Imagine filtrar uma lista: você quer ler a lista completa, mas também quer poder manter um estado separado com filtros e retornar uma lista filtrada. O Jotai permite isso com uma função de escrita como segundo argumento.
import { atom } from 'jotai';
const allTodosAtom = atom([
{ id: 1, text: 'Estudar', completed: true },
{ id: 2, text: 'Exercitar', completed: false },
{ id: 3, text: 'Descansar', completed: false }
]);
const filterAtom = atom('all'); // 'all', 'completed', 'active'
// Atom derivado com lógica de leitura E escrita
const filteredTodosAtom = atom(
(get) => {
const todos = get(allTodosAtom);
const filter = get(filterAtom);
switch (filter) {
case 'completed':
return todos.filter(t => t.completed);
case 'active':
return todos.filter(t => !t.completed);
default:
return todos;
}
},
// Segunda função: escrever em atoms derivados
(get, set, newTodos) => {
set(allTodosAtom, newTodos);
}
);
function TodoApp() {
const [todos] = useAtom(filteredTodosAtom);
const [filter, setFilter] = useAtom(filterAtom);
return (
<div>
<div>
<button onClick={() => setFilter('all')}>Todos</button>
<button onClick={() => setFilter('active')}>Pendentes</button>
<button onClick={() => setFilter('completed')}>Completos</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Async Atoms: Integrando Requisições HTTP e Operações Assíncronas
O Padrão Básico de Async Atoms
Trabalhar com dados assíncronos em uma aplicação real é inevitável. Jotai torna isso elegante permitindo que atoms retornem Promises. Quando você usa um async atom, o hook useAtom() ainda funciona normalmente, mas o valor pode estar pendente (loading), completado (com dados), ou em erro.
import { atom } from 'jotai';
// Um atom simples que dispara uma requisição
const userDataAtom = atom(async () => {
const response = await fetch('/api/user/1');
if (!response.ok) throw new Error('Falha ao buscar usuário');
return response.json();
});
function UserProfile() {
const [user] = useAtom(userDataAtom);
// Se user for uma Promise, a suspensão automática de Jotai
// vai pausar a renderização até a Promise resolver
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Você precisa envolver em um Suspense e ErrorBoundary
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<p>Carregando usuário...</p>}>
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
</Suspense>
);
}
Async Atoms Parametrizados
Um padrão comum é ter async atoms que dependem de parâmetros — como buscar um produto baseado em um ID. Você pode implementar isso combinando um atom de parâmetro com um atom derivado assíncro.
import { atom } from 'jotai';
const productIdAtom = atom('1');
const productAtom = atom(async (get) => {
const id = get(productIdAtom);
const response = await fetch(`/api/products/${id}`);
if (!response.ok) throw new Error('Produto não encontrado');
return response.json();
});
function ProductDetail() {
const [productId, setProductId] = useAtom(productIdAtom);
const [product] = useAtom(productAtom);
return (
<div>
<input
value={productId}
onChange={(e) => setProductId(e.target.value)}
placeholder="ID do produto"
/>
<div>
<h2>{product.name}</h2>
<p>Preço: R$ {product.price}</p>
<p>{product.description}</p>
</div>
</div>
);
}
Quando productIdAtom muda, o Jotai automaticamente recalcula productAtom e dispara uma nova requisição. Não há necessidade de efeitos manually ou callbacks.
Tratamento de Erros em Async Atoms
O Jotai oferece a função atomWithDefault() e outras utilidades, mas a forma mais simples e explícita de lidar com erros é usar uma estrutura de dados que capture tanto sucesso quanto fracasso.
import { atom } from 'jotai';
const apiDataAtom = atom(async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return { status: 'success', data: await response.json() };
} catch (error) {
return {
status: 'error',
error: error instanceof Error ? error.message : 'Erro desconhecido'
};
}
});
function DataDisplay() {
const [result] = useAtom(apiDataAtom);
if (result.status === 'error') {
return <div style={{ color: 'red' }}>Erro: {result.error}</div>;
}
return (
<div>
<pre>{JSON.stringify(result.data, null, 2)}</pre>
</div>
);
}
Cache e Refetch em Async Atoms
Para casos onde você quer fazer refetch manualmente ou implementar cache inteligente, combine async atoms com useAtomCallback().
import { atom, useAtomCallback } from 'jotai';
const apiResponseAtom = atom(null);
const isLoadingAtom = atom(false);
const fetchDataAtom = useAtomCallback((get, set) => async () => {
set(isLoadingAtom, true);
try {
const response = await fetch('/api/items');
const data = await response.json();
set(apiResponseAtom, data);
} catch (error) {
set(apiResponseAtom, null);
console.error('Erro:', error);
} finally {
set(isLoadingAtom, false);
}
});
function DataManager() {
const [data] = useAtom(apiResponseAtom);
const [isLoading] = useAtom(isLoadingAtom);
const refetch = useAtomCallback(fetchDataAtom);
return (
<div>
<button onClick={() => refetch()} disabled={isLoading}>
{isLoading ? 'Carregando...' : 'Refetch'}
</button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
Conclusão
Os três pilares que você aprendeu aqui — Atoms, Derived State e Async Atoms — formam a base para criar aplicações React escaláveis com estado gerenciado de forma limpa e performática. Atoms são suas unidades de estado minimalistas; derived atoms eliminam a necessidade de duplicar lógica de computação; e async atoms trazem requisições HTTP e operações assíncronas para dentro do mesmo modelo de state management, sem complicações extras.
A principal vantagem do Jotai em relação a outras soluções é a modularidade e granularidade: você define exatamente o que precisa, sem boilerplate, e a biblioteca se encarrega de otimizações. Não há ações, reducers ou despachadores — apenas estado e cálculos sobre esse estado.
Comece pequeno em seus projetos: crie alguns atoms para features isoladas, entenda como derived atoms economizam lógica, depois explore async atoms para integrar com sua API. O Jotai escala naturalmente conforme sua aplicação cresce.