React & Frontend

Boas Práticas de Jotai em Profundidade: Atoms, Derived State e Async Atoms para Times Ágeis

13 min de leitura

Boas Práticas de Jotai em Profundidade: Atoms, Derived State e Async Atoms para Times Ágeis

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

<h2>Entendendo Jotai: Uma Alternativa Elegante ao Redux</h2>

<p>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 <strong>atoms</strong> — 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.</p>

<p>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.</p>

<h2>Atoms: Construindo Blocos de Estado</h2>

<h3>O Conceito Fundamental de um Atom</h3>

<p>Um atom em Jotai é simplesmente um objeto que representa uma unidade de estado. Você cria atoms usando a função <code>atom()</code> 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.</p>

<pre><code class="language-javascript">import { atom } from &#039;jotai&#039;;

// Um atom simples com valor primitivo

const countAtom = atom(0);

// Um atom com um objeto

const userAtom = atom({

name: &#039;João&#039;,

email: &#039;joao@example.com&#039;,

age: 28

});

// Um atom com um array

const todoAtom = atom([

{ id: 1, text: &#039;Aprender Jotai&#039;, completed: false },

{ id: 2, text: &#039;Usar em produção&#039;, completed: false }

]);</code></pre>

<h3>Lendo e Escrevendo em Atoms</h3>

<p>Para interagir com atoms em componentes React, você usa o hook <code>useAtom()</code>. Esse hook retorna um array com dois elementos: o valor atual do atom e uma função para atualizar esse valor. É similar ao <code>useState()</code>, mas o estado é compartilhado globalmente entre todos os componentes que usam o mesmo atom.</p>

<pre><code class="language-javascript">import { useAtom } from &#039;jotai&#039;;

import { countAtom } from &#039;./atoms&#039;;

function Counter() {

const [count, setCount] = useAtom(countAtom);

return (

&lt;div&gt;

&lt;p&gt;Contagem: {count}&lt;/p&gt;

&lt;button onClick={() =&gt; setCount(count + 1)}&gt;Incrementar&lt;/button&gt;

&lt;button onClick={() =&gt; setCount(c =&gt; c - 1)}&gt;Decrementar&lt;/button&gt;

&lt;/div&gt;

);

}</code></pre>

<p>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 <code>useState()</code>. Isso permite padrões funcionais elegantes quando você precisa fazer cálculos baseados no estado anterior.</p>

<h3>Operações Avançadas com Atoms</h3>

<p>À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 <code>useAtomCallback()</code> para esses casos mais complexos.</p>

<pre><code class="language-javascript">import { atom, useAtomCallback } from &#039;jotai&#039;;

const firstNameAtom = atom(&#039;João&#039;);

const lastNameAtom = atom(&#039;Silva&#039;);

const fullNameAtom = atom(&#039;João Silva&#039;);

function ProfileUpdater() {

const updateProfile = useAtomCallback(

(get, set) =&gt; async (first, last) =&gt; {

// 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(&#039;/api/profile&#039;, {

method: &#039;POST&#039;,

body: JSON.stringify({ first, last })

});

}

);

return (

&lt;button onClick={() =&gt; updateProfile(&#039;Carlos&#039;, &#039;Santos&#039;)}&gt;

Atualizar Perfil

&lt;/button&gt;

);

}</code></pre>

<h2>Derived State: Computando Novos Estados a Partir de Atoms Existentes</h2>

<h3>Atoms Derivados com <code>atom()</code></h3>

<p>Um dos conceitos mais poderosos do Jotai é a capacidade de criar <strong>atoms derivados</strong> — atoms que dependem de outros atoms e são calculados automaticamente. Quando você passa uma função de leitura como argumento para <code>atom()</code>, está criando um atom que computa seu valor baseado em outros atoms.</p>

<pre><code class="language-javascript">import { atom } from &#039;jotai&#039;;

const priceAtom = atom(100);

const quantityAtom = atom(5);

// Atom derivado que calcula o total

const totalAtom = atom(

(get) =&gt; get(priceAtom) * get(quantityAtom)

);

function ShoppingCart() {

const [price] = useAtom(priceAtom);

const [quantity] = useAtom(quantityAtom);

const [total] = useAtom(totalAtom);

return (

&lt;div&gt;

&lt;p&gt;Preço: R$ {price}&lt;/p&gt;

&lt;p&gt;Quantidade: {quantity}&lt;/p&gt;

&lt;p style={{ fontWeight: &#039;bold&#039; }}&gt;Total: R$ {total}&lt;/p&gt;

&lt;/div&gt;

);

}</code></pre>

<p>A chave aqui é que <code>totalAtom</code> não armazena seu próprio estado. Ele é <strong>read-only</strong> por padrão — apenas calcula seu valor sob demanda. Quando <code>priceAtom</code> ou <code>quantityAtom</code> mudam, o Jotai automaticamente recalcula <code>totalAtom</code> e notifica apenas os componentes que o usam.</p>

<h3>Atoms Derivados com Lógica de Escrita</h3>

<p>À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.</p>

<pre><code class="language-javascript">import { atom } from &#039;jotai&#039;;

const allTodosAtom = atom([

{ id: 1, text: &#039;Estudar&#039;, completed: true },

{ id: 2, text: &#039;Exercitar&#039;, completed: false },

{ id: 3, text: &#039;Descansar&#039;, completed: false }

]);

const filterAtom = atom(&#039;all&#039;); // &#039;all&#039;, &#039;completed&#039;, &#039;active&#039;

// Atom derivado com lógica de leitura E escrita

const filteredTodosAtom = atom(

(get) =&gt; {

const todos = get(allTodosAtom);

const filter = get(filterAtom);

switch (filter) {

case &#039;completed&#039;:

return todos.filter(t =&gt; t.completed);

case &#039;active&#039;:

return todos.filter(t =&gt; !t.completed);

default:

return todos;

}

},

// Segunda função: escrever em atoms derivados

(get, set, newTodos) =&gt; {

set(allTodosAtom, newTodos);

}

);

function TodoApp() {

const [todos] = useAtom(filteredTodosAtom);

const [filter, setFilter] = useAtom(filterAtom);

return (

&lt;div&gt;

&lt;div&gt;

&lt;button onClick={() =&gt; setFilter(&#039;all&#039;)}&gt;Todos&lt;/button&gt;

&lt;button onClick={() =&gt; setFilter(&#039;active&#039;)}&gt;Pendentes&lt;/button&gt;

&lt;button onClick={() =&gt; setFilter(&#039;completed&#039;)}&gt;Completos&lt;/button&gt;

&lt;/div&gt;

&lt;ul&gt;

{todos.map(todo =&gt; (

&lt;li key={todo.id} style={{

textDecoration: todo.completed ? &#039;line-through&#039; : &#039;none&#039;

}}&gt;

{todo.text}

&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}</code></pre>

<h2>Async Atoms: Integrando Requisições HTTP e Operações Assíncronas</h2>

<h3>O Padrão Básico de Async Atoms</h3>

<p>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 <code>useAtom()</code> ainda funciona normalmente, mas o valor pode estar pendente (loading), completado (com dados), ou em erro.</p>

<pre><code class="language-javascript">import { atom } from &#039;jotai&#039;;

// Um atom simples que dispara uma requisição

const userDataAtom = atom(async () =&gt; {

const response = await fetch(&#039;/api/user/1&#039;);

if (!response.ok) throw new Error(&#039;Falha ao buscar usuário&#039;);

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 (

&lt;div&gt;

&lt;h1&gt;{user.name}&lt;/h1&gt;

&lt;p&gt;{user.email}&lt;/p&gt;

&lt;/div&gt;

);

}

// Você precisa envolver em um Suspense e ErrorBoundary

import { Suspense } from &#039;react&#039;;

function App() {

return (

&lt;Suspense fallback={&lt;p&gt;Carregando usuário...&lt;/p&gt;}&gt;

&lt;ErrorBoundary&gt;

&lt;UserProfile /&gt;

&lt;/ErrorBoundary&gt;

&lt;/Suspense&gt;

);

}</code></pre>

<h3>Async Atoms Parametrizados</h3>

<p>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.</p>

<pre><code class="language-javascript">import { atom } from &#039;jotai&#039;;

const productIdAtom = atom(&#039;1&#039;);

const productAtom = atom(async (get) =&gt; {

const id = get(productIdAtom);

const response = await fetch(/api/products/${id});

if (!response.ok) throw new Error(&#039;Produto não encontrado&#039;);

return response.json();

});

function ProductDetail() {

const [productId, setProductId] = useAtom(productIdAtom);

const [product] = useAtom(productAtom);

return (

&lt;div&gt;

&lt;input

value={productId}

onChange={(e) =&gt; setProductId(e.target.value)}

placeholder=&quot;ID do produto&quot;

/&gt;

&lt;div&gt;

&lt;h2&gt;{product.name}&lt;/h2&gt;

&lt;p&gt;Preço: R$ {product.price}&lt;/p&gt;

&lt;p&gt;{product.description}&lt;/p&gt;

&lt;/div&gt;

&lt;/div&gt;

);

}</code></pre>

<p>Quando <code>productIdAtom</code> muda, o Jotai automaticamente recalcula <code>productAtom</code> e dispara uma nova requisição. Não há necessidade de efeitos manually ou callbacks.</p>

<h3>Tratamento de Erros em Async Atoms</h3>

<p>O Jotai oferece a função <code>atomWithDefault()</code> 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.</p>

<pre><code class="language-javascript">import { atom } from &#039;jotai&#039;;

const apiDataAtom = atom(async () =&gt; {

try {

const response = await fetch(&#039;/api/data&#039;);

if (!response.ok) throw new Error(HTTP ${response.status});

return { status: &#039;success&#039;, data: await response.json() };

} catch (error) {

return {

status: &#039;error&#039;,

error: error instanceof Error ? error.message : &#039;Erro desconhecido&#039;

};

}

});

function DataDisplay() {

const [result] = useAtom(apiDataAtom);

if (result.status === &#039;error&#039;) {

return &lt;div style={{ color: &#039;red&#039; }}&gt;Erro: {result.error}&lt;/div&gt;;

}

return (

&lt;div&gt;

&lt;pre&gt;{JSON.stringify(result.data, null, 2)}&lt;/pre&gt;

&lt;/div&gt;

);

}</code></pre>

<h3>Cache e Refetch em Async Atoms</h3>

<p>Para casos onde você quer fazer refetch manualmente ou implementar cache inteligente, combine async atoms com <code>useAtomCallback()</code>.</p>

<pre><code class="language-javascript">import { atom, useAtomCallback } from &#039;jotai&#039;;

const apiResponseAtom = atom(null);

const isLoadingAtom = atom(false);

const fetchDataAtom = useAtomCallback((get, set) =&gt; async () =&gt; {

set(isLoadingAtom, true);

try {

const response = await fetch(&#039;/api/items&#039;);

const data = await response.json();

set(apiResponseAtom, data);

} catch (error) {

set(apiResponseAtom, null);

console.error(&#039;Erro:&#039;, error);

} finally {

set(isLoadingAtom, false);

}

});

function DataManager() {

const [data] = useAtom(apiResponseAtom);

const [isLoading] = useAtom(isLoadingAtom);

const refetch = useAtomCallback(fetchDataAtom);

return (

&lt;div&gt;

&lt;button onClick={() =&gt; refetch()} disabled={isLoading}&gt;

{isLoading ? &#039;Carregando...&#039; : &#039;Refetch&#039;}

&lt;/button&gt;

{data &amp;&amp; &lt;pre&gt;{JSON.stringify(data, null, 2)}&lt;/pre&gt;}

&lt;/div&gt;

);

}</code></pre>

<h2>Conclusão</h2>

<p>Os três pilares que você aprendeu aqui — <strong>Atoms, Derived State e Async Atoms</strong> — 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.</p>

<p>A principal vantagem do Jotai em relação a outras soluções é a <strong>modularidade e granularidade</strong>: 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.</p>

<p>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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://jotai.org/" target="_blank" rel="noopener noreferrer">Documentação Oficial do Jotai</a></li>

<li><a href="https://github.com/pmndrs/jotai" target="_blank" rel="noopener noreferrer">Jotai - GitHub Repository</a></li>

<li><a href="https://blog.logrocket.com/jotai-guide-react-state-management/" target="_blank" rel="noopener noreferrer">React State Management com Jotai - LogRocket</a></li>

<li><a href="https://jotai.org/docs/basics/primitives" target="_blank" rel="noopener noreferrer">Atoms and Derived State - Jotai Docs</a></li>

<li><a href="https://jotai.org/docs/api/core#async" target="_blank" rel="noopener noreferrer">Async in Jotai - Jotai API Reference</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Guia Completo de useSyncExternalStore: Integrando Stores Externas com React
Guia Completo de useSyncExternalStore: Integrando Stores Externas com React

O Problema: Estado Externo e React Quando trabalhamos com React, frequentemen...

Dominando XState em React: State Machines e Statecharts na Prática em Projetos Reais
Dominando XState em React: State Machines e Statecharts na Prática em Projetos Reais

O Que São State Machines e Por Que Devemos Usar? Uma máquina de estados é um...

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado
React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases React Fib...