wandres.dev
JOTAI A FONDO · estado atómico

El modelo atómico: el átomo como unidad mínima

Jotai no es un store pequeño sino la negación del store como punto de partida. Esta lección disecciona el átomo como definición sin valor, la separación entre configuración e instancia que lo vuelve reutilizable, y la inversión topológica que supone construir el estado de abajo arriba en vez de tallarlo desde un objeto central. Queda claro por qué la identidad referencial del átomo, y no una clave de texto, es la decisión que volvió practicable el modelo que Recoil solo llegó a esbozar, y qué precio cobra la dispersión cuando se atomiza sin criterio.

⏱ 19 min

Todo el track ha girado alrededor de una figura: un contenedor único donde vive el estado y del que los componentes extraen porciones. Redux la canonizó, Redux Toolkit la hizo soportable, Zustand la adelgazó hasta un hook. Jotai hace algo distinto: no reduce el store, lo elimina como punto de partida. En su lugar propone una unidad mínima —el átomo— que se declara sola, sin pertenecer a nada, y que se combina con otras para formar estructuras mayores. La diferencia no es de tamaño sino de dirección: donde el store se talla de arriba abajo, el grafo de átomos se cultiva de abajo arriba. Entender esa inversión, y sobre todo entender que un átomo no contiene su propio valor, es la llave que abre las cuatro lecciones siguientes.

🎯 Al terminar esta lección sabrás
  • Distinguir el átomo como definición del valor que un store guarda para él.
  • Comprender la inversión topológica entre el modelo descendente del store y el ascendente de los átomos.
  • Explicar por qué la identidad referencial sustituye a las claves de texto de Recoil y qué habilita esa decisión.
  • Elegir la suscripción mínima con useAtom, useAtomValue y useSetAtom según la intención del componente.

Un átomo no guarda su valor

La afirmación que más resistencia genera al llegar a Jotai es también la más importante: un átomo no contiene un valor. Lo que devuelve atom(0) no es una caja con un cero dentro, sino un objeto de configuración —una descripción de cómo obtener un valor inicial y, en su caso, cómo escribirlo— que actúa además como su propia clave. El valor real vive en un store, una estructura de tipo mapa que asocia cada definición de átomo con su estado actual dentro de un árbol concreto de React.

import { atom } from 'jotai'

// esto es una definicion, no un contenedor: no hay ningun cero guardado aqui
export const contadorAtom = atom(0)

// la identidad del objeto ES la clave: dos llamadas producen dos atomos distintos
const a = atom(0)
const b = atom(0)
// a !== b, aunque su valor inicial coincida

La consecuencia inmediata es que un átomo declarado a nivel de módulo no es estado global en el sentido tradicional del término. No hay nada que se comparta por el hecho de importarlo; lo que se comparte es la coordenada, y el valor al que esa coordenada apunta depende del store contra el que se resuelva. Es exactamente la misma relación que existe entre una clave y un diccionario: la clave puede citarse en cualquier parte sin comprometer a nadie, porque solo cobra significado dentro de un diccionario concreto.

Esa separación entre definición e instancia es la que explica comportamientos que de otro modo parecerían mágicos. Un mismo contadorAtom declarado una sola vez a nivel de módulo puede tener el valor siete en un subárbol y el valor cero en otro, si cada subárbol tiene su propio store; puede reiniciarse por completo desmontando un Provider; puede existir en el servidor con un valor por petición sin contaminar a los demás usuarios. La lección cinco explota esa propiedad hasta el final, pero conviene fijarla ya: la definición es estable y compartida, el valor es contextual.

La analogía útil aquí no es la de una variable sino la de un esquema de base de datos. Nadie espera que la definición de una tabla contenga filas: la definición describe la forma y las filas viven en una instancia concreta del motor. Un átomo ocupa exactamente ese papel respecto del store, y la mayor parte de la confusión inicial procede de haberlo leído como si fuera una variable global con azúcar reactivo por encima.

ℹ️
La identidad referencial como clave

Recoil, el experimento de Meta que popularizó el modelo atómico, exigía una clave de texto única y global por átomo. Ese registro manual era una fuente constante de colisiones, obligaba a coordinar nombres entre módulos y peleaba con el code splitting, porque dos fragmentos cargados por separado podían reclamar la misma clave. Jotai elimina el problema entero usando la referencia del objeto como identidad. La consecuencia no es cosmética: componer deja de cobrar un impuesto administrativo, y una utilidad que fabrica átomos puede generar miles sin inventar un solo nombre. Cuando componer es gratis, la composición escala.

Hay una consecuencia de higiene que se aprecia sobre todo en equipos grandes. Como el átomo no transporta valor, importarlo desde cualquier módulo es una operación sin efectos: no inicializa nada, no reserva memoria en ningún store y no crea acoplamiento temporal entre módulos. Los módulos que solo declaran átomos son, en la práctica, módulos de datos inertes, y esa inercia elimina toda una familia de problemas de orden de carga que aquejan a los stores construidos en tiempo de importación.

De abajo arriba: la inversión topológica

Un store impone una decisión temprana y difícil de revertir: la forma del todo. Antes de escribir el primer selector hay que decidir qué campos existen, cómo se agrupan en slices y quién es dueño de qué. Esa forma global luego se recorre hacia abajo con selectores, y cada componente vive en la tensión de extraer lo mínimo de una estructura que no fue diseñada para él. El modelo atómico invierte el orden de las obligaciones: se declaran fragmentos independientes que no saben unos de otros, y la estructura emerge después, dibujada por las dependencias que unos declaran sobre otros.

flowchart TD
subgraph Descendente
  S[store central] --> P1[porcion a]
  S --> P2[porcion b]
  S --> P3[porcion c]
end
subgraph Ascendente
  A[atomo precio] --> D[atomo subtotal]
  B[atomo cantidad] --> D
  D --> T[atomo total]
end
style S fill:#f38ba8,color:#11111b
style T fill:#a6e3a1,color:#11111b

La consecuencia práctica más subestimada de esta inversión es el camino de crecimiento. En el modelo descendente, promover un estado local a global implica reescritura: sacarlo del componente, encontrarle un hueco en la forma global, escribir un selector y una acción. En el modelo ascendente ese camino no existe porque no hay forma global que respetar. Un useState que de pronto necesita compartirse se convierte en un átomo cambiando la declaración, y los componentes lejanos lo leen sin que nadie levante estado a un ancestro común ni lo baje por props a través de capas que no lo usan.

Esa ausencia de forma global tiene además un efecto sobre la legibilidad del acoplamiento. En un store, el acoplamiento es implícito: todo convive en el mismo objeto y solo la disciplina impide que cualquier parte lea cualquier otra. Con átomos, el acoplamiento es explícito y local, porque un átomo derivado declara exactamente de qué depende en el acto de leerlo. El grafo entero se vuelve auditable pieza a pieza, sin necesidad de cargar la estructura completa en la cabeza para razonar sobre un fragmento.

Hay también una consecuencia sobre el reparto de código que suele decidirse tarde y duele caro. Un store central es, por construcción, un módulo que todas las rutas importan, y por tanto un fragmento que siempre viaja en el paquete inicial aunque la mayoría de sus slices no se usen en la primera pantalla. Los átomos no tienen ese problema: cada uno es un módulo pequeño que solo se carga si alguien lo importa, de modo que el estado se reparte por el grafo de dependencias del empaquetador con la misma granularidad que los componentes que lo consumen. La atomización no solo divide el estado en tiempo de ejecución, también lo divide en tiempo de carga.

💡
Leer el diagrama en las dos direcciones

El diagrama admite dos lecturas y ambas enseñan algo. De arriba abajo, el store reparte: quien lo diseña decide qué existe y los consumidores se adaptan a esa oferta. De abajo arriba, los átomos convergen: nadie decidió que hubiera un total, el total apareció porque alguien necesitó combinar dos valores existentes. La segunda lectura describe mejor cómo crecen realmente los productos, donde los requisitos llegan de uno en uno y rara vez respetan la forma que se fijó al principio.

La suscripción como intención

Esa promoción sin reescritura tiene además un efecto sobre el momento de las decisiones que conviene valorar en términos de diseño y no solo de comodidad. Con un store central, decidir qué es global se hace pronto y con poca información, porque la estructura hay que fijarla antes de poder usarla; con átomos, esa decisión se aplaza hasta que la evidencia la reclama, y aplazar decisiones hasta tener información es, en cualquier disciplina de ingeniería, preferible a tomarlas temprano por obligación estructural.

La API de consumo imita deliberadamente a la de useState, pero la desdobla en tres hooks porque la intención del componente es información valiosa que el sistema puede aprovechar. useAtom devuelve la pareja de valor y actualizador y suscribe a los cambios. useAtomValue solo lee. useSetAtom solo escribe y, crucialmente, no suscribe a nada: un componente que únicamente despacha no se vuelve a renderizar nunca por causa de ese átomo.

import { useAtom, useAtomValue, useSetAtom } from 'jotai'

function Panel() {
  const contador = useAtomValue(contadorAtom)   // lee: re-render si cambia
  const incrementar = useSetAtom(contadorAtom)  // escribe: cero suscripciones
  return <button onClick={() => incrementar((c) => c + 1)}>{contador}</button>
}

Elegir el hook mínimo no es una microoptimización opcional sino la forma idiomática de que la granularidad del grafo se traduzca en trabajo real de renderizado. Un botón de acción escrito con useAtom arrastra una suscripción que no necesita y se renderiza con cada cambio del contador; el mismo botón con useSetAtom es inmune. La granularidad que el modelo atómico promete solo se materializa si el consumo la respeta.

💡
La suscripción es por átomo, no por store

La diferencia con un store plano es estructural y no de grado. Cuando un componente selecciona una porción de un store, el sistema debe notificarle cada cambio del store entero y comparar el resultado del selector para decidir si el renderizado procede: el trabajo de comparación crece con el número de suscriptores, aunque el cambio afecte a uno solo. Con átomos no hay nada que comparar porque no hay nada que notificar de más: el escritor conoce las aristas salientes del átomo escrito y despierta exclusivamente a quienes están en ellas. La optimización de lectura deja de ser una técnica que se aplica y pasa a ser una propiedad de la estructura.

⚛️

Definición estable

El átomo se declara una vez a nivel de módulo y se importa sin miedo: no lleva valor consigo, así que no hay estado compartido accidental entre árboles.

🗺️

Valor contextual

El store asocia definición y valor. Cambiar de store cambia todos los valores sin tocar una sola declaración.

🧩

Composición sin registro

La identidad es la referencia, no un nombre. Fabricar átomos en tiempo de ejecución no exige inventar claves ni evitar colisiones.

🎯

Suscripción mínima

Leer, escribir o ambas cosas son intenciones distintas con hooks distintos, y el sistema cobra exactamente por la que declaras.

Conviene resistir aquí una simplificación frecuente. Que el sistema despierte a menos componentes no significa que la aplicación sea automáticamente más rápida: significa que el trabajo de renderizado se vuelve proporcional a lo que de verdad cambió. Esa proporcionalidad es lo valioso, porque es la propiedad que se conserva cuando la aplicación crece, mientras que cualquier optimización aplicada a mano se degrada en cuanto alguien añade un consumidor que no la conocía.

Lo que el modelo cobra

Sería deshonesto presentar la atomización como ganancia sin contrapartida. El store tiene una virtud que el grafo pierde: un lugar único donde mirar. Abrir el archivo del store y ver la forma completa del estado de una aplicación es un acto de comprensión que no tiene equivalente cuando ese mismo estado vive repartido en sesenta átomos declarados en veinte módulos. La dispersión es el modo de fallo característico del modelo atómico, y no se resuelve con una utilidad sino con disciplina de colocación y nomenclatura.

Conviene además calibrar la comparación honestamente, porque la dispersión no es un defecto exclusivo del grafo. Un store central bien mantenido es legible de un vistazo, pero un store central de tres años, con cuarenta slices escritos por gente distinta y campos que nadie recuerda quién introdujo, no ofrece esa legibilidad prometida: ofrece un archivo enorme donde el orden es tipográfico y no conceptual. La ventaja del store no es tener un lugar único, sino que ese lugar mantenga una forma comprensible, y esa condición se pierde con el tiempo salvo que alguien la defienda activamente. La diferencia real, entonces, es dónde se paga la disciplina: en el grafo se paga distribuyendo bien los módulos, en el store se paga defendiendo la forma del objeto central.

📝
Colocación por dominio, no por tipo

La convención que mejor contiene la dispersión es la misma que ordena cualquier base de código madura: agrupar por dominio y no por naturaleza técnica. Un módulo que reúna los átomos del carrito junto a sus derivaciones y sus acciones es navegable; un módulo que reúna todos los átomos primitivos de la aplicación porque comparten tipo es un catálogo sin significado. La regla práctica que se deriva de ahí es que un átomo debería vivir tan cerca de sus consumidores como su alcance permita, y ascender a un módulo compartido solo cuando la evidencia demuestre que varios dominios lo necesitan.

El segundo coste es de trazabilidad. Redux compró su ceremonia a cambio de una bitácora: toda mutación pasaba por una acción con nombre, y esa acción quedaba registrada, era reproducible y permitía viajar en el tiempo. En el modelo atómico, una escritura es una llamada directa sobre un átomo desde cualquier punto del árbol, y sin convenciones propias no queda registro de quién la hizo ni por qué. Los átomos de acción de la lección siguiente recuperan buena parte de esa trazabilidad al dar nombre a las transiciones, pero recuperarla es una decisión que hay que tomar, no un regalo del modelo.

Ninguno de los dos costes invalida el modelo; lo delimitan. La conclusión operativa es que el grafo atómico rinde allí donde hay derivación real y aislamiento que ganar, y que fuera de ese territorio conviene seguir prefiriendo un store plano por la comprensión que regala. Las cuatro lecciones siguientes recorren precisamente ese territorio: el grafo derivado, las familias parametrizadas, la asincronía integrada y el aislamiento por ámbitos. Cada una explota una consecuencia distinta de la misma idea inicial, la de que una definición y un valor son cosas separadas.

⚠️
Atomizar no es trocear

El error frecuente al adoptar Jotai es convertir cada variable en un átomo por reflejo. Un booleano de tema y una bandera de menú abierto no ganan nada por vivir en átomos separados si nadie deriva de ellos: solo añaden ruido. El modelo atómico rinde cuando existe un grafo real de dependencias, es decir, cuando unos valores son función de otros y la recomputación selectiva importa. Sin grafo, un átomo suelto es un useState global con más ceremonia, y el store plano de Zustand sigue siendo la respuesta mejor ajustada.

La unidad de composición decide la arquitectura

Hay una pregunta que precede a la elección de cualquier librería de estado y que casi nadie se hace explícitamente: cuál es la unidad de composición del sistema. En Redux, la unidad es el reducer, y todo lo demás —acciones, middleware, selectores— existe para alimentarlo y consultarlo; por eso su arquitectura es inevitablemente centralizada, porque componer reducers significa combinarlos en uno mayor y ese acto siempre converge hacia un vértice. En Jotai, la unidad es el átomo, y componer átomos significa que uno lea a otros, un acto que no converge en ningún vértice sino que teje una red. Esa diferencia en la unidad mínima determina todo lo demás: determina si el estado tiene un dueño o muchos, si el crecimiento es reestructuración o adición, si el aislamiento es una excepción que hay que construir o la topología por defecto. Quien elige una librería mirando su tamaño o su ergonomía superficial está mirando síntomas; quien la elige mirando su unidad de composición está eligiendo la forma que su sistema tendrá dentro de dos años, porque las unidades componen según su propia lógica y esa lógica es más fuerte que cualquier convención que se le imponga encima. El paso de madurez que pide esta lección es aprender a leer una librería por su unidad mínima y a preguntarse si la forma en que esa unidad se combina coincide con la forma del problema. Cuando el problema es una red de dependencias —celdas que se calculan, campos que se validan entre sí, totales que se deducen— la coincidencia con el átomo es exacta y el sistema casi se escribe solo. Cuando el problema es un puñado de valores planos sin relaciones, esa misma unidad multiplica las piezas sin ganar nada, y elegirla es haber confundido potencia con adecuación.

⚔️ Separa la definición del valor
  1. Declara tres átomos primitivos en un módulo aparte y compruébalos con una igualdad estricta: dos llamadas a atom con el mismo valor inicial producen objetos distintos. Explica por qué esa desigualdad es exactamente lo que permite prescindir de claves.
  2. Consume el mismo átomo desde dos componentes hermanos y verifica que comparten valor. Después envuelve uno de ellos en un Provider y observa cómo la misma definición pasa a tener dos valores independientes.
  3. Escribe un componente que solo despacha, primero con useAtom y luego con useSetAtom. Instrumenta el renderizado con un contador y mide la diferencia real de re-renders.
  4. Toma una pieza de estado local que hoy vive en useState y prómuévela a átomo sin mover nada de sitio. Anota cuántas líneas cambiaron y compáralo con lo que habría costado la misma promoción en un store central.
  5. Recorre tu aplicación y clasifica cinco piezas de estado en dos columnas: valores planos sin dependientes y nodos de un grafo de derivación. Justifica cada clasificación antes de decidir la herramienta.
  6. Diseña una convención de colocación de átomos para tu base de código —por dominio, por ruta, por feature— y escríbela. Sin ella, la dispersión que la lección advierte llega antes de lo que parece.
  7. Revisa qué entra hoy en tu paquete inicial por culpa del store y estima cuánto de ese peso desaparecería si cada pieza de estado viajase con el módulo que la consume.
  8. Escribe en una frase, para cada átomo que hayas creado, qué significa su valor en términos del dominio. Los que no admitan esa frase probablemente no debían ser átomos.