Jotai: estado atómico de abajo arriba
Donde Zustand ofrece un store centralizado más ligero, Jotai invierte la dirección del pensamiento: en vez de un árbol grande del que extraes porciones, construyes el estado desde piezas mínimas —átomos— que se componen hacia arriba. Los átomos derivados forman un grafo de dependencias que se recalcula solo, lo que vuelve a Jotai la elección natural cuando el estado es interdependiente y derivado. Todo en unos 4KB y sin las claves string que hundieron a Recoil.
Zustand hizo el store más ligero, pero seguía siendo un store: un objeto grande y central del que los componentes extraen porciones de arriba abajo. Jotai, también de Poimandres y obra de Daishi Kato, invierte la dirección. En vez de partir de un todo y seleccionar partes, partes de piezas mínimas —los átomos— y las compones hacia arriba. El estado deja de ser un árbol que talas y pasa a ser un grafo que cultivas: átomos primitivos en la base, átomos derivados que dependen de ellos, y una máquina de recálculo que mantiene el grafo consistente sin que muevas un dedo. En unos 4KB, es la respuesta idiomática cuando el estado es interdependiente y derivado.
- Declarar átomos primitivos con
atomy consumirlos conuseAtom,useAtomValueyuseSetAtom. - Componer átomos derivados de solo lectura y de lectura-escritura como grafo de dependencias.
- Entender el modelo de abajo arriba y por qué los átomos no guardan valor: lo guarda el store.
- Modelar estado asíncrono con átomos y reconocer cuándo el problema pide átomos y no un store.
El átomo primitivo y el grafo derivado
Un átomo es una unidad mínima de estado. El primitivo se declara con un valor inicial; el derivado se declara con una función que lee otros átomos mediante get y devuelve un valor computado. Esa función define una arista en el grafo de dependencias:
import { atom } from 'jotai'
export const contadorAtom = atom(0) // primitivo
export const dobleAtom = atom((get) => get(contadorAtom) * 2) // derivado de solo lectura
export const parAtom = atom((get) => get(contadorAtom) % 2 === 0)
La propiedad crucial: dobleAtom y parAtom no se recalculan nunca por su cuenta. Jotai sabe que dependen de contadorAtom porque lo leyeron con get, así que solo los recomputa cuando contadorAtom cambia, y solo notifica a los componentes que consumen precisamente esos derivados. La memoización de valores derivados —que en React harías a mano con useMemo y sus arrays de dependencias, siempre a riesgo de olvidar una— aquí es la mecánica por defecto del sistema, y las dependencias se descubren solas en cada ejecución en vez de declararse.
flowchart TD P[contadorAtom primitivo] --> D[dobleAtom derivado] P --> E[parAtom derivado] D --> V[vista A] E --> W[vista B] P --> X[vista C] style P fill:#89b4fa,color:#11111b style D fill:#a6e3a1,color:#11111b style E fill:#a6e3a1,color:#11111b
Consumir átomos en componentes
La API imita deliberadamente a useState, y esa familiaridad es parte de su diseño: reducir la fricción de adopción. useAtom devuelve la pareja valor y actualizador; cuando solo lees, useAtomValue evita suscribir la escritura; cuando solo escribes, useSetAtom evita re-renderizar por lecturas que no te importan:
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
function Contador() {
const [contador, setContador] = useAtom(contadorAtom)
const doble = useAtomValue(dobleAtom)
return <button onClick={() => setContador((c) => c + 1)}>{contador} / {doble}</button>
}
Los átomos derivados pueden ser también de escritura, definiendo una segunda función que recibe get y set y traduce una intención en mutaciones de los átomos base. Este patrón —el átomo de acción— encapsula transiciones complejas detrás de una interfaz simple, y es el equivalente atómico de una acción de Zustand:
export const incrementarAtom = atom(
null, // sin valor de lectura: es una accion
(get, set) => set(contadorAtom, get(contadorAtom) + 1),
)
La distinción entre useAtom, useAtomValue y useSetAtom no es cosmética: codifica tu intención y el sistema la aprovecha. Un componente que solo despacha una acción con useSetAtom no se suscribe a ningún valor, así que no se re-renderiza jamás por cambios de estado; uno que solo lee con useAtomValue no arrastra un actualizador que no usa. Elegir el hook mínimo para cada componente es la forma idiomática de que la granularidad del grafo se traduzca en re-renders mínimos, sin trabajo extra por tu parte.
La migración desde useState es casi mecánica, y eso no es casual sino un objetivo de diseño: un estado local que de pronto necesita compartirse entre componentes lejanos se promueve de useState a un átomo cambiando una línea y sin levantar el estado por toda la jerarquía. Desaparece el prop drilling y desaparece también el clásico levantamiento de estado al ancestro común, que acoplaba componentes solo para compartir un dato entre dos hojas distantes del árbol.
Ese camino de crecimiento —de local a global sin reescritura— es una de las razones prácticas de la adopción de Jotai en bases que empiezan pequeñas y crecen. No pagas por adelantado una infraestructura global; conviertes en global solo lo que la evidencia demuestra que debe serlo, y la conversión es barata justo en el momento en que hace falta.
El giro conceptual que más cuesta: un átomo no contiene un valor, es una definición —una configuración con identidad propia—. El valor real vive en un store, por defecto uno global implícito, y opcionalmente en uno provisto por un Provider para aislar estado por subárbol o por petición en SSR. Un mismo atom puede así tener valores distintos en dos Provider distintos sin tocar su declaración. Por eso los átomos se declaran una vez a nivel de módulo y se reutilizan sin miedo: son la forma del estado, no el estado. Entender esta separación entre definición y valor es la llave de todo Jotai.
De abajo arriba: cuándo los átomos ganan
La diferencia con Zustand no es de peso sino de dirección del diseño. Un store es de arriba abajo: decides la forma del todo y extraes partes. Los átomos son de abajo arriba: defines fragmentos independientes y dejas que las dependencias entre ellos dibujen la estructura. Esa inversión tiene una consecuencia práctica profunda. En un store, el acoplamiento es implícito —todo vive en el mismo objeto y depende de que tú lo mantengas ordenado—; con átomos, el acoplamiento es explícito y local —un derivado declara exactamente de qué depende y de nada más—, de modo que el grafo entero es legible pieza a pieza sin cargar el todo en la cabeza.
Esto brilla exactamente donde Zustand incomoda: cuando el estado es una red de valores que dependen unos de otros y cuya recomputación selectiva importa. El caso canónico es el estado derivado en cascada —un filtro que afecta a una lista, que afecta a un total, que afecta a un resumen— donde cada eslabón debe recalcularse si y solo si su fuente cambió. Modelado como átomos, el grafo se ocupa de la propagación mínima; modelado como un store plano, tú vigilas a mano qué depende de qué y cuándo invalidar. Cuanto más denso el tejido de dependencias, mayor la ventaja de los átomos.
Hay un coste honesto en este poder: la dispersión. Un estado modelado como cincuenta átomos sueltos puede costar más de abarcar de un vistazo que un único store donde todo está a la vista, y sin disciplina de nomenclatura y colocación el grafo degenera en un laberinto de dependencias implícitas. Jotai premia a quien piensa en grafos y castiga a quien solo trocea; la atomicidad no es gratis, es una inversión que rinde cuando el problema de verdad tiene forma de red y se desperdicia cuando no.
Ese descubrimiento dinámico de dependencias, además, es más potente de lo que parece. Si un derivado lee un átomo dentro de una rama condicional, la dependencia solo existe cuando esa rama se ejecuta, y el grafo se reconfigura solo según el camino tomado. No hay array de dependencias que mantener sincronizado con el cuerpo de la función, esa fuente perenne de bugs en useMemo y useEffect: el grafo es siempre exacto porque se reconstruye de la ejecución real, no de una lista que prometiste mantener al día.
El núcleo es minimalista, pero jotai/utils cubre los patrones frecuentes sin inflarlo: atomWithStorage persiste un átomo, atomFamily genera átomos parametrizados por un argumento —un átomo por cada id—, selectAtom deriva una porción con comparación propia, y splitAtom descompone un átomo de lista en un átomo por elemento, ideal para listas grandes donde cada fila debe re-renderizar por su cuenta. Igual que en Zustand, la política es núcleo pequeño más utilidades opcionales: pagas peso solo por lo que compones.
Un ejemplo concreto vale más que la lista. Un átomo persistido y una porción derivada con comparación propia se declaran así, y participan en el grafo como cualquier otro átomo:
import { atomWithStorage, selectAtom } from 'jotai/utils'
export const preferenciasAtom = atomWithStorage('prefs', { tema: 'oscuro', idioma: 'es' })
export const temaAtom = selectAtom(preferenciasAtom, (p) => p.tema)
temaAtom solo notifica a sus consumidores cuando tema cambia, aunque idioma cambie mil veces: la granularidad del grafo llega hasta el campo. atomWithStorage, además, sincroniza con almacenamiento y entre pestañas sin que escribas una línea de coordinación.
Átomos asíncronos y el grafo que respira
El golpe de gracia del modelo es que la asincronía es solo otro nodo del grafo. Un átomo cuya función de lectura es async devuelve una promesa, y Jotai la integra con Suspense de React: el componente que lo consume suspende hasta que resuelve, sin banderas de loading a mano.
export const usuarioAtom = atom(async (get) => {
const id = get(usuarioIdAtom)
const res = await fetch(`/api/usuarios/${id}`)
return res.json() // el componente suspende hasta resolver
})
Lo potente es que este átomo async participa en el grafo como cualquier otro: si usuarioIdAtom cambia, la petición se relanza sola y los derivados que dependían del usuario se recomputan en cascada. Combinado con atomFamily, obtienes una caché parametrizada por identidad sin escribir una línea de gestión de caché:
import { atomFamily } from 'jotai/utils'
export const tareaAtom = atomFamily((id: string) =>
atom(async (get) => {
const res = await fetch(`/api/tareas/${id}`)
return res.json()
}),
)
Dicho esto, para estado de servidor serio —con revalidación, reintentos y sincronización— sigue siendo mejor una herramienta dedicada como TanStack Query; los átomos async brillan cuando ese estado remoto debe entretejerse con estado de cliente derivado en el mismo grafo.
Merece subrayarse la elegancia de que la asincronía no sea un caso especial. En Redux, un dato asíncrono exigía middleware —thunks o sagas— y una máquina de estados de petición escrita a mano. En Jotai, una promesa es un valor como cualquier otro dentro del grafo, y Suspense se encarga del estado de carga. La reducción conceptual es enorme: un solo mecanismo, el átomo, cubre estado síncrono, derivado y asíncrono, y las tres formas se componen entre sí sin costuras ni adaptadores.
Esa uniformidad tiene además un efecto pedagógico que no conviene pasar por alto. Una vez entiendes el átomo, entiendes todo Jotai: no hay una API para lo local, otra para lo derivado y otra para lo async, como ocurría en la maraña de conceptos de Redux. La curva de aprendizaje es corta porque la superficie conceptual es minúscula, y esa economía de ideas es, en sí misma, una forma de potencia: menos que aprender, menos que olvidar, menos donde equivocarse.
Para entender Jotai hay que resistir la tentación de verlo como un useState global con esteroides y verlo como lo que es: un motor de programación reactiva donde la unidad de composición es el átomo. La pregunta que Jotai responde mejor que nadie no es dónde guardo el estado sino cómo expreso que este valor depende de aquellos otros sin recalcular de más ni de menos. Recoil, de Meta, había planteado esta misma visión atómica primero, pero la lastró con una decisión fatal: cada átomo exigía una clave string única y global, un registro manual propenso a colisiones que envenenaba la composición y el code splitting. Jotai hizo la jugada decisiva de usar la identidad del propio objeto átomo como su clave —la referencia es la identidad— eliminando de golpe todo ese registro. Esa simplificación, aparentemente menor, es la que volvió práctica la visión atómica y la que sobrevivió cuando Meta archivó Recoil. La lección de diseño trasciende la librería: la composición solo escala cuando componer no cobra un impuesto administrativo, y una clave que hay que inventar y no repetir es exactamente ese impuesto. Al hacer que la identidad emerja del objeto y no de un nombre, Jotai convirtió los átomos en piezas verdaderamente componibles, del mismo modo que una función pura se compone sin registrarla en ningún catálogo. Cuando tu estado es un grafo de dependencias y no un cajón de valores sueltos, esta es la herramienta cuyo modelo mental coincide con la forma del problema, y esa coincidencia —no el tamaño en kilobytes— es la verdadera razón para elegirla. Elegir Jotai para un par de banderas globales sería tan desajustado como elegir Redux para ellas: la potencia del grafo solo se paga cuando hay grafo que modelar.
- Declara tres átomos primitivos: una lista de tareas, un texto de filtro y un booleano de mostrar solo pendientes. Ninguno debe saber de los otros.
- Construye un átomo derivado
tareasVisiblesAtomque combine los tres, y sobre él otroresumenAtomque cuente visibles y totales. Dibuja el grafo de dependencias en papel antes de mirarlo funcionar. - Consume
resumenAtomconuseAtomValueen un componente y cambia solo el filtro. Verifica que se recalcula la cascada pero que los componentes que no dependen de ella no se re-renderizan. - Convierte una acción compleja —marcar todas como hechas— en un átomo de escritura con
nullde lectura, y compárala en claridad con hacerlo desde fuera manipulando el átomo de lista directamente. - Añade un átomo async que cargue las tareas de un servidor y hazlo depender de un
usuarioIdAtom. Cambia el id y confirma que la petición se relanza y la cascada entera se recomputa sola. - Promueve un
useStatelocal a un átomo compartido cambiando solo la declaración, sin levantar el estado a ningún ancestro. Ese camino de crecimiento sin reescritura es una de las razones prácticas para empezar con átomos.