Jotai: estado interdependiente y derivado
Donde Zustand construye un store de arriba abajo, Jotai compone el estado de abajo arriba a partir de átomos, y esa inversión de topología no es un detalle de estilo: es lo que lo hace idóneo para el estado interdependiente y derivado. Esta lección construye átomos primitivos y derivados, explica el grafo de dependencias de grano fino que solo recomputa la cadena afectada, y presenta atomFamily como la herramienta para colecciones dinámicas —celdas de una hoja de cálculo, campos de un form builder, valores computados finos—. Jotai es, en la práctica, el modelo de signals traído a la capa de estado de React, y reconocer cuándo tu problema es un grafo de derivaciones y no un puñado de banderas planas es lo que decide entre él y Zustand.
Zustand resolvió el estado global plano con elegancia, pero hay una clase de problema donde su forma —un objeto central del que seleccionas porciones— empieza a estorbar: el estado en el que unos valores se derivan de otros formando una red de dependencias. Un configurador de precios donde el total depende de la cantidad, el descuento del total, los impuestos del subtotal. Un form builder donde la validez de un campo depende de otros tres. Una hoja de cálculo donde una celda es una fórmula sobre celdas vecinas. Para eso existe Jotai, y su diferencia con Zustand no es de tamaño sino de topología: en vez de un store del que bajas, Jotai compone el estado de abajo arriba, a partir de piezas mínimas llamadas átomos que se enlazan en un grafo. Es, en esencia, el modelo de signals que viste en el Nivel 4, aterrizado en la capa de estado de React.
- Distinguir el modelo atómico de abajo arriba de
Jotaidel store de arriba abajo deZustand. - Componer átomos derivados y entender el grafo de dependencias de grano fino que forman.
- Usar
atomFamilypara colecciones dinámicas: celdas, campos, filas. - Reconocer con precisión el nicho de
Jotai: estado interdependiente y derivado, no el global plano.
Átomos: el estado se compone de abajo arriba
Un átomo es la unidad mínima de estado en Jotai: una pieza de valor que los componentes pueden leer y a la que pueden suscribirse. Los hay de dos clases, y la segunda es donde reside toda la potencia. Un átomo primitivo guarda un valor, como atom(100). Un átomo derivado no guarda nada: se define por una función que lee otros átomos con get, y se recomputa solo cuando alguno de los que leyó cambia. Esa función de derivación es la que teje el grafo.
import { atom } from 'jotai'
const precioAtom = atom(100)
const cantidadAtom = atom(2)
// atomo derivado: no guarda valor, se recomputa cuando precio o cantidad cambian
const subtotalAtom = atom((get) => get(precioAtom) * get(cantidadAtom))
// la derivacion encadena: los impuestos dependen del subtotal, que depende de dos atomos
const ivaAtom = atom((get) => get(subtotalAtom) * 0.21)
const totalAtom = atom((get) => get(subtotalAtom) + get(ivaAtom))
Compara la topología con la de Zustand. Allí declaras un objeto central y desde los componentes bajas a sus porciones con selectores; el store es la cima y todo cuelga de él. Aquí no hay cima: hay muchas piezas pequeñas —precioAtom, cantidadAtom— y las compones hacia arriba para formar subtotalAtom, y sobre este ivaAtom y totalAtom. La dirección está invertida. Consumir un átomo en un componente usa hooks familiares, pero con una consecuencia de rendimiento que es el corazón del asunto:
import { useAtomValue } from 'jotai'
function Total() {
const total = useAtomValue(totalAtom) // re-render solo si totalAtom cambia
return <span>{total}</span>
}
Los átomos derivados que solo leen son la mitad de la historia. Un átomo derivado puede además escribir si le das una segunda función (get, set, valor) que traduce una actualización externa en escrituras sobre los átomos fuente. Es el patrón para exponer una API de escritura que por dentro coordina varios átomos primitivos manteniendo una sola fuente de verdad.
import { atom } from 'jotai'
const celsiusAtom = atom(20)
// derivado de lectura y escritura: leer da fahrenheit, escribir vuelve a celsius
const fahrenheitAtom = atom(
(get) => get(celsiusAtom) * 9 / 5 + 32,
(_get, set, nuevoF: number) => set(celsiusAtom, (nuevoF - 32) * 5 / 9),
)
Ese átomo se lee en grados Fahrenheit pero se escribe en Celsius, y ambas vistas quedan siempre coherentes porque solo hay una fuente de verdad —celsiusAtom— y la otra es pura derivación bidireccional. Es el principio de minimizar el estado real, que atraviesa todo el track, aplicado a dos representaciones del mismo valor.
El grafo de dependencias de grano fino
Cuando escribes en cantidadAtom, Jotai no recomputa todo: recorre el grafo y recomputa únicamente la cadena que depende de él —subtotalAtom, luego ivaAtom y totalAtom— y vuelve a renderizar solo los componentes suscritos a alguno de esos átomos. Un componente que solo lee precioAtom no se entera. Esta es exactamente la propagación de grano fino de los signals: el grafo sabe quién depende de quién, así que el cambio viaja por las aristas y se detiene donde deja de haber dependientes.
Ese recorrido no es ingenuo. Jotai propaga en orden topológico, de modo que un átomo se recomputa una sola vez aunque varias de sus dependencias hayan cambiado a la vez, y nunca con un valor intermedio inconsistente. Es la propiedad glitch-free del Nivel 8, garantizada por el mismo motor que resuelve el diamante de dependencias: si totalAtom depende de subtotalAtom y de ivaAtom, y este último también del subtotal, totalAtom espera a que ambos padres estén actualizados antes de calcularse, y así evita el parpadeo de un total computado con un IVA viejo.
flowchart TD P[precioAtom] --> S[subtotalAtom] C[cantidadAtom] --> S S --> IVA[ivaAtom] S --> T[totalAtom] IVA --> T D[descuentoAtom] --> T style S fill:#89b4fa,color:#11111b style T fill:#cba6f7,color:#11111b
La diferencia con Zustand se vuelve tangible al escalar. Imagina mil valores que se derivan unos de otros. En un store central tendrías que recalcular las derivaciones a mano dentro de las acciones, o memoizar con selectores que tú mismo mantienes; cada cambio te obliga a razonar qué más recalcular. En Jotai esa contabilidad la lleva el grafo: defines cada derivación una vez, declarativamente, y el sistema garantiza que se recompute justo cuando toca y ni una vez de más. Por eso la frase que resume su nicho es precisa: Jotai no sirve para guardar valores, sino para modelar relaciones entre valores.
Un átomo derivado cachea su resultado y solo lo recalcula si cambian sus dependencias reales, medido por identidad. Si subtotalAtom da el mismo número, ivaAtom no se recomputa aunque otro átomo lejano haya cambiado. Es la misma memoización de los valores computados del Nivel 6, pero gratis y automática: no eliges qué memoizar ni gestionas listas de dependencias como en useMemo; el grafo deduce las dependencias del acto de leer con get. Esa deducción automática es lo que hace que la derivación en Jotai escale sin que tú lleves la cuenta.
atomFamily: colecciones dinámicas
El grafo estático de arriba —unos pocos átomos con nombre— cubre un configurador de precios, pero no una hoja de cálculo con diez mil celdas ni un form builder que crea campos en tiempo de ejecución. Para eso está atomFamily: una función que fabrica un átomo por cada clave, memoizando de modo que la misma clave siempre devuelve el mismo átomo. Cada celda, cada campo, cada fila obtiene su propio átomo independiente.
import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// un atomo primitivo independiente por cada id de celda
const celdaAtom = atomFamily((id: string) => atom(''))
// una celda calculada es un atomo derivado que lee otras celdas por su id
const celdaCalculadaAtom = atomFamily((id: string) =>
atom((get) => evaluarFormula(get(celdaAtom(id)))),
)
Esta es la característica que convierte a Jotai en la herramienta idiomática para form builders y hojas de cálculo. Como cada celda es su propio átomo, editar una no toca a las demás: solo se recomputan las celdas cuya fórmula la referencia, y solo se renderizan los componentes de esas celdas. En un store central, todas las celdas vivirían en un mismo objeto y aislarlas requeriría un trabajo de selección y memoización que crece con la cuadrícula. Con atomFamily, el aislamiento es la topología por defecto: mil campos son mil suscripciones independientes, no una gigante que se dispara con cada tecla.
Un detalle que multiplica el alcance: un átomo derivado puede ser asíncrono. Si su función devuelve una promesa, Jotai integra el resultado con Suspense, de modo que un componente que lee un átomo async suspende hasta que resuelve, sin que gestiones estados de carga a mano. Combinado con atomFamily, esto permite colecciones donde cada elemento se resuelve de forma independiente y perezosa: cada celda pide su dato solo cuando alguien la observa, y el grafo encadena esas resoluciones asíncronas igual que encadena las síncronas.
Jotai frente a Zustand: dos topologías, no dos rivales
Es un error frecuente plantear Jotai y Zustand como competidores donde uno debe ganar. No lo son: resuelven formas distintas del problema, y elegir bien es reconocer la forma del tuyo. Zustand es de arriba abajo: un store central, ideal cuando el estado global es un puñado de valores planos que muchos componentes leen —tema, sesión, banderas de UI—. Jotai es de abajo arriba: muchos átomos pequeños que se componen, ideal cuando el estado es una red de interdependencias o una colección dinámica con derivación fina. Usar Zustand para una hoja de cálculo es pelear contra su topología; usar Jotai para guardar un booleano de tema es fragmentar sin motivo.
Hojas de cálculo
Miles de celdas, cada una un átomo; una fórmula recomputa solo su cadena. La topología de atomFamily es la de la propia cuadrícula.
Form builders
Campos creados en tiempo de ejecución con validaciones que dependen entre sí. Cada campo aislado en su átomo no re-renderiza a sus vecinos.
Valores computados finos
Totales, resúmenes y banderas derivadas de otros datos. Se declaran una vez y no pueden discrepar de sus fuentes.
Asistentes por pasos
Un wizard donde un paso habilita a otro: el grafo expresa las dependencias entre pasos sin un reducer central que las coordine.
El modelo atómico lo popularizó Recoil, el experimento de Meta, pero Recoil quedó abandonado y Jotai recogió la idea, la depuró y se convirtió en su implementación de referencia en 2026. La convergencia es más profunda de lo que parece: el grafo de átomos derivados de Jotai es el mismo modelo que la propuesta de signals de TC39 que viste en el Nivel 4, solo que expuesto como estado de React. Si aquel nivel te dejó la intuición del grafo reactivo, Jotai es esa intuición hecha librería de estado, y aprender uno refuerza el otro.
El modelo atómico tiene su propio modo de fallar. Si atomizas todo, el estado se dispersa en decenas de átomos sueltos por el código y razonar sobre el conjunto se vuelve difícil: no hay un lugar único donde mirar para saber qué existe, como sí lo hay en un store de Zustand. La regla que lo contiene es la misma del árbol de decisión: usa átomos donde la derivación fina y el aislamiento son el objetivo, no como reemplazo universal del store. Un booleano de tema en un átomo suelto es tan mala idea como una hoja de cálculo aplanada en un objeto de Zustand; cada herramienta paga caro salirse de su topología.
El salto de madurez que pide esta lección es dejar de ver todo el estado como valores que se guardan y empezar a distinguir cuándo lo que tienes entre manos es en realidad una red de relaciones. Un booleano de tema es un valor: existe por sí solo, no depende de nada, y guardarlo en un store plano es la respuesta perfecta. El total de un carrito no es un valor: es una relación —una función de precios, cantidades, descuentos e impuestos— y tratarlo como un valor que hay que recalcular y sincronizar a mano es la fuente de una clase entera de bugs, esos en los que el descuento cambió pero el total se quedó viejo porque alguien olvidó recalcularlo. Jotai y el modelo de signals que encarna existen para que las relaciones se declaren una vez, como derivaciones, y el sistema garantice su consistencia para siempre: el total nunca puede discrepar del subtotal porque no es un dato aparte que sincronizar, es una función que se reevalúa sola cuando sus entradas cambian. Ahí está la inversión conceptual que separa a quien colecciona librerías de quien diseña estado: el primero pregunta qué librería usar para guardar el total; el segundo se da cuenta de que el total no debe guardarse en absoluto, porque no es estado, es derivación, y el único estado verdadero son las entradas de las que todo lo demás se deduce. Reconocer esa frontera —qué es fuente y qué es consecuencia— minimiza el estado que de verdad gobiernas y hace imposibles, por construcción, las inconsistencias entre un valor y su derivado. Cuando tu problema tiene esa forma de grafo, Jotai no es una preferencia estética sobre Zustand: es la herramienta cuya topología coincide con la del problema, y usar cualquier otra es traducir a mano lo que ella hace por diseño.
- Elige un cálculo real de tu app con al menos tres niveles de dependencia —por ejemplo subtotal, impuestos y total— y escríbelo como átomos primitivos y derivados.
- Suscribe un componente solo al átomo intermedio y otro solo al final. Cambia una entrada y observa que cada uno se renderiza únicamente si su átomo cambió.
- Introduce a propósito la versión mala: guarda el total como un átomo primitivo que actualizas a mano. Provoca la inconsistencia olvidando actualizarlo y comprueba qué bug produce.
- Convierte una colección —filas de una tabla editable, campos de un formulario dinámico— en un
atomFamilyy verifica que editar un elemento no re-renderiza los demás. - Añade una celda calculada con
atomFamilyque lea otras por su id, al estilo de una hoja de cálculo, y comprueba que solo se recomputa su cadena de dependencias. - Decide honestamente, para tres piezas de estado de tu app, si son valores planos —van a
Zustand— o grafos de relaciones —van aJotai—. La forma del problema, no la moda, decide.