Provider y scopes: aislar árboles de átomos
La separación entre definición y valor cobra su recompensa aquí: un mismo átomo puede tener tantos valores como stores existan. Esta lección explica el store implícito y cuándo deja de bastar, cómo un Provider crea un ámbito aislado que permite instanciar el mismo grafo varias veces, por qué el SSR exige un store por petición para no filtrar datos entre usuarios, y cómo esa misma propiedad convierte los tests en algo trivial de aislar y de sembrar.
La primera lección insistió en que un átomo no guarda su valor. Es aquí donde esa afirmación deja de ser una curiosidad conceptual y se convierte en la característica más práctica del modelo. Si la definición vive en el módulo y el valor vive en un store, entonces basta con cambiar de store para que el mismo grafo de átomos tenga otro conjunto de valores, sin tocar una línea de sus declaraciones. Eso permite instanciar el mismo formulario tres veces en la misma página sin que compartan estado, aislar cada petición en el servidor para que ningún usuario vea datos de otro, y montar un test con estado sembrado y garantizado limpio. El mecanismo que hace todo esto es un componente sencillo, Provider, y una función que fabrica stores. La sutileza no está en la API sino en saber cuándo el ámbito global implícito deja de ser suficiente.
- Distinguir el store implícito por defecto del store explícito creado con
createStore. - Aislar subárboles con
Providerpara instanciar el mismo grafo de átomos varias veces. - Diseñar un store por petición en SSR y explicar qué se filtra si se omite.
- Sembrar valores iniciales con
useHydrateAtomsy aislar tests sin recurrir a reinicios globales.
El store implícito y cuándo deja de bastar
Sin ningún Provider, los átomos operan contra un store por defecto que existe a nivel de módulo y es único para todo el proceso. Para una aplicación de cliente con un solo árbol y un solo usuario, esa comodidad es correcta y no hay razón para complicarla. El problema aparece en tres situaciones concretas: cuando el mismo grafo debe existir varias veces a la vez, cuando el proceso atiende a varios usuarios simultáneamente, y cuando varias pruebas comparten el mismo proceso.
import { Provider, createStore } from 'jotai'
// un store explicito es un objeto con get, set y sub: se puede usar fuera de React
const store = createStore()
store.set(temaAtom, 'claro')
console.log(store.get(temaAtom))
function App() {
return (
<Provider store={store}>
<Contenido />
</Provider>
)
}
El store explícito tiene un valor añadido que suele pasar desapercibido: es utilizable fuera de React. Leer y escribir átomos desde un manejador de eventos del navegador, desde un enrutador o desde una capa de integración no obliga a montar componentes, lo que resulta esencial para conectar el grafo con código imperativo que no vive en el árbol.
No hace falta pasar un store para obtener aislamiento: un Provider sin propiedades crea uno nuevo para su subárbol, con el ciclo de vida del propio componente. Al desmontarse, ese store desaparece con todos sus valores, lo que convierte el desmontaje en un reinicio completo y limpio del estado del subárbol. Es la forma más económica de obtener un reinicio sin escribir ninguna acción de reinicio, y explica por qué el patrón de cambiar la clave de React de un Provider para forzar su recreación es una técnica habitual para descartar un formulario entero.
Un grafo, muchas instancias
El caso que mejor ilustra el ámbito es un componente complejo que debe aparecer varias veces en la misma pantalla. Un panel de filtros, un editor incrustado, una ficha comparativa: cada instancia necesita su propio estado, pero la lógica —los átomos y sus derivaciones— debe escribirse una sola vez. En un store central esto obliga a parametrizar todo el estado por un identificador de instancia y a arrastrar ese identificador por cada selector y cada acción. Con ámbitos, las declaraciones no cambian en absoluto.
flowchart TD D[definiciones de atomos] --> S1[store instancia A] D --> S2[store instancia B] S1 --> V1[panel izquierdo] S2 --> V2[panel derecho] D --> SG[store global implicito] SG --> VG[cabecera compartida] style D fill:#89b4fa,color:#11111b style SG fill:#f9e2af,color:#11111b
function Comparador() {
return (
<div>
<Provider>
<PanelDeFiltros />
</Provider>
<Provider>
<PanelDeFiltros />
</Provider>
</div>
)
}
Conviene entender la regla de resolución que hace esto posible, porque es la fuente de los errores más desconcertantes. Cuando un componente consume un átomo, el valor se busca en el Provider ancestro más cercano; si no hay ninguno, se usa el store implícito. Esto significa que un componente que quedó fuera del Provider por accidente seguirá funcionando, pero contra el store global, y el síntoma será un estado que no se comparte con sus hermanos sin ningún error visible. El aislamiento es silencioso en ambas direcciones.
El store implícito vive en el ámbito del módulo, y esa es exactamente la propiedad que lo vuelve peligroso fuera del navegador. Un servidor mantiene el módulo cargado entre peticiones, de modo que todo lo que se escriba en el store implícito persiste para la siguiente petición y, por tanto, para el siguiente usuario. Un átomo de sesión escrito durante la petición de un usuario puede ser leído durante la de otro. No es un fallo de rendimiento ni una rareza de hidratación: es una filtración de datos entre usuarios, y la única defensa es no permitir jamás que el código de servidor toque el store implícito.
Un store por petición
La regla en renderizado de servidor es breve y no admite excepciones: cada petición crea su propio store y lo entrega a un Provider que envuelve el árbol que se va a renderizar. Con eso, dos peticiones concurrentes ejercitan el mismo grafo de definiciones sobre dos conjuntos de valores completamente separados, y no existe ningún camino por el que una pueda observar a la otra.
import { Provider, createStore } from 'jotai'
export function renderizarPeticion(datosDeSesion: Sesion) {
const store = createStore()
store.set(sesionAtom, datosDeSesion) // siembra explicita, aislada por peticion
return (
<Provider store={store}>
<App />
</Provider>
)
}
La siembra de valores iniciales merece un tratamiento propio dentro de componentes, porque hacerla en un efecto llega tarde: el primer renderizado ya habría ocurrido con el valor por defecto, produciendo el desajuste de hidratación que la lección anterior describía. La utilidad de hidratación resuelve esto escribiendo los valores antes del primer renderizado del subárbol.
import { useHydrateAtoms } from 'jotai/utils'
function Sembrado({ usuario, children }: Props) {
useHydrateAtoms([[usuarioAtom, usuario]]) // ocurre antes del primer render
return <>{children}</>
}
Tests: el aislamiento por construcción
El mismo mecanismo convierte las pruebas en un problema resuelto. La dificultad clásica de probar estado global es la contaminación entre casos: una prueba deja un valor escrito y la siguiente lo hereda, produciendo fallos que dependen del orden de ejecución y desaparecen al ejecutar el caso aislado. La respuesta habitual es una función de reinicio global que hay que recordar invocar y mantener al día conforme el estado crece. Con ámbitos, esa función no hace falta: cada prueba crea su store, y la separación es estructural en vez de convenida.
import { createStore, Provider } from 'jotai'
test('el total refleja el descuento aplicado', () => {
const store = createStore()
store.set(precioAtom, 100)
store.set(descuentoAtom, 0.1)
expect(store.get(totalAtom)).toBeCloseTo(108.9) // sin montar nada de React
})
La segunda mitad de esa prueba es la más significativa: el grafo se puede ejercitar sin React en absoluto. Un store explícito permite escribir entradas, leer derivados y suscribirse a cambios como si fuera una librería de estado independiente, lo que hace que la lógica de dominio expresada en derivaciones se pruebe con la velocidad y la simplicidad de una prueba unitaria pura, dejando el renderizado para las pruebas que de verdad lo necesitan.
Store implícito
Cómodo en cliente, inaceptable en servidor. Vive en el módulo y sobrevive entre peticiones.
Ámbito por subárbol
Un Provider aísla y su desmontaje reinicia. El mismo grafo instanciado tantas veces como haga falta.
Store por petición
La única topología segura en SSR. Sin ella, el estado de un usuario alcanza al siguiente.
Prueba sin React
Escribir entradas y leer derivados contra un store desnudo convierte el dominio en pruebas unitarias puras.
Que un sistema de estado permita instanciar su grafo varias veces parece una comodidad menor hasta que se advierte lo que implica su ausencia. Un estado que solo puede existir una vez por proceso obliga a que toda la aplicación sea singular: no hay dos formularios iguales, no hay dos peticiones concurrentes seguras, no hay dos pruebas independientes, y cada uno de esos casos debe resolverse añadiendo un identificador de instancia a cada pieza del estado y propagándolo por todas partes. Esa parametrización manual es una forma de ámbito reconstruida a mano, más frágil y más ruidosa, y su existencia en tantos códigos es la mejor prueba de que el ámbito era una necesidad real que el diseño no había reconocido. La lección general trasciende a Jotai y vale para cualquier sistema: separar la definición de la instancia es lo que permite que una misma descripción se realice muchas veces, y esa separación es la misma idea que subyace a las clases frente a los objetos, a los esquemas frente a las filas y a los componentes frente a sus montajes. Cuando un diseño la omite, el estado global se vuelve un recurso singular y compartido, es decir, exactamente la clase de recurso que la ingeniería concurrente lleva medio siglo aprendiendo a evitar. Y hay un último matiz que conviene no perder: la seguridad en servidor no es una funcionalidad añadida al modelo de ámbitos sino una consecuencia directa de haberlo hecho bien. Quien entiende que la filtración entre usuarios y la imposibilidad de aislar un test son el mismo problema visto desde dos ángulos ha entendido por qué la distinción entre definición y valor era, desde la primera lección, la idea que sostenía todo lo demás.
- Toma un componente con estado que hoy aparece una sola vez y colócalo dos veces en la misma pantalla sin envolverlo. Observa cómo comparten estado y después aísla cada uno con su propio
Provider. - Fuerza el error silencioso: deja un componente fuera del
Providerque sus hermanos usan. Documenta el síntoma y explica por qué no aparece ningún error. - Usa la clave de React sobre un
Providerpara descartar y recrear un formulario completo. Compara esa técnica con escribir una acción de reinicio que ponga cada átomo en su valor inicial. - Escribe una prueba que ejercite tres niveles de derivación contra un store creado con
createStore, sin montar ningún componente. Mide cuánto tarda frente a la misma prueba con renderizado. - Construye dos pruebas que escriban el mismo átomo y ejecútalas primero contra el store implícito, comprobando la contaminación, y después cada una con su propio store.
- En una página renderizada en servidor, siembra un valor por petición con la utilidad de hidratación y verifica que no hay desajuste. Después escribe deliberadamente en el store implícito desde el servidor y razona qué habría ocurrido con dos usuarios concurrentes.