Context + store: estado compartido reactivo de subárbol
El patrón canónico de estado compartido en Solid: un Provider que crea un store con createStore, arma la tupla estado más acciones y la provee por el contexto. Los consumidores leen estado reactivo de grano fino y mutan solo por acciones. El store vive atado al owner del Provider, así que el alcance es el subárbol —y cada instancia del Provider tiene su propio estado.
Aquí convergen los dos hilos del nivel. El contexto sabe llevar un valor a un subárbol; el store sabe ser estado reactivo de grano fino. Unirlos produce el patrón más importante de la gestión de estado en Solid: un Provider que crea un store por dentro, expone una tupla de estado y acciones, y la reparte por el árbol. Es el equivalente idiomático —y más simple— de lo que en otros ecosistemas se monta con librerías enteras: tema, autenticación, carrito, cualquier estado que un subárbol comparte, resuelto con dos primitivos que ya conoces.
- Construir un
Providerque cree unstoreinterno y provea la tupla[estado, acciones]. - Encapsular la mutación en acciones que cierran sobre
setStore, dejando el estado de solo lectura. - Entender por qué el
storeda reactividad de grano fino que los consumidores aprovechan. - Ver que el estado vive atado al owner del
Provider: alcance de subárbol e instancias propias.
El patrón canónico
La estructura es siempre la misma y conviene memorizarla como una plantilla. Un componente Provider crea el store, define las acciones que lo mutan, ensambla la tupla y la pasa como value. Un hook useX da acceso validado. El objeto de contexto se queda privado al módulo. Es la contrapartida con createStore del patrón factory del nivel anterior: allí el store se empaquetaba con getters; aquí exponemos la tupla [estado, acciones] del proxy.
import { createContext, useContext, type ParentComponent } from "solid-js";
import { createStore, type Store } from "solid-js/store";
interface Linea { id: string; nombre: string; precio: number; cantidad: number }
interface EstadoCarrito { lineas: Linea[] }
type CarritoValor = [
Store<EstadoCarrito>,
{
agregar: (l: Linea) => void;
quitar: (id: string) => void;
vaciar: () => void;
},
];
const CarritoContext = createContext<CarritoValor>();
El tipo ya lo dominamos de la lección anterior: estado de solo lectura para el consumidor, mutación exclusiva por acciones. Ahora el Provider que lo puebla.
El Provider que crea el store
export const CarritoProvider: ParentComponent = (props) => {
const [estado, setEstado] = createStore<EstadoCarrito>({ lineas: [] });
const acciones = {
agregar(l: Linea) {
const i = estado.lineas.findIndex((x) => x.id === l.id);
if (i >= 0) setEstado("lineas", i, "cantidad", (c) => c + l.cantidad);
else setEstado("lineas", (ls) => [...ls, l]);
},
quitar(id: string) {
setEstado("lineas", (ls) => ls.filter((x) => x.id !== id));
},
vaciar() {
setEstado("lineas", []);
},
};
return (
<CarritoContext.Provider value={[estado, acciones]}>
{props.children}
</CarritoContext.Provider>
);
};
export function useCarrito() {
const ctx = useContext(CarritoContext);
if (!ctx) throw new Error("useCarrito requiere un CarritoProvider");
return ctx;
}
Tres detalles cargan todo el peso. createStore vive dentro del Provider, así que su ciclo de vida queda atado al owner de ese componente. Las acciones cierran sobre setEstado: el consumidor nunca ve el setter, solo verbos de dominio —agregar, quitar, vaciar— que expresan intención, no mecánica. Y el value es una referencia estable —la tupla— cuya reactividad no está en su identidad sino en el store que contiene: recuerda que el contexto se fija una vez, y aquí eso es justo lo correcto, porque lo que cambia es el interior del store, no la tupla.
Consumir: reactividad de grano fino
El consumidor llama al hook, desestructura estado y acciones, y usa ambos con naturalidad. La reactividad de grano fino del store hace el resto.
function ResumenCarrito() {
const [carrito, { vaciar }] = useCarrito();
return (
<div>
<span>Artículos: {carrito.lineas.length}</span>
<button onClick={vaciar}>Vaciar</button>
</div>
);
}
Aquí está la joya que Solid regala y que en React costaría cuidado manual: este componente solo se recomputa en la parte que leyó. carrito.lineas.length es una lectura de grano fino; cuando cambia la cantidad de una línea, se actualiza ese span y nada más. Otro componente que lea carrito.lineas[2].precio no se inmuta si cambia la línea 0. No hay re-render del consumidor, no hay comparación de props, no hay memoización defensiva: el store notifica exactamente a quien leyó exactamente lo que cambió.
flowchart TD PROV[CarritoProvider crea el store] --> CTX[Context lleva la tupla estado y acciones] CTX --> R[ResumenCarrito lee lineas length] CTX --> L[ListaLineas lee cada linea] CTX --> B[BotonAgregar usa la accion agregar] B -->|muta via setEstado| PROV PROV -.grano fino notifica solo al lector afectado.-> L style PROV fill:#89b4fa,color:#11111b style B fill:#fab387,color:#11111b style L fill:#a6e3a1,color:#11111b
Podrías proveer varios signals sueltos, pero el store gana en cuanto el estado es un objeto o una lista. Un store ofrece reactividad de grano fino sobre estado anidado: leer estado.lineas[i].cantidad suscribe solo a esa hoja, y setEstado con su ruta actualiza solo esa hoja. Un solo contexto puede así albergar un estado rico —decenas de campos anidados— sin que tocar uno despierte a los lectores de otro. Con signals sueltos tendrías que trocear el estado a mano y proveer muchos; el store lo hace por ti y mantiene el contexto en una sola tupla limpia.
Si varios consumidores necesitan el mismo cálculo —el total del carrito, el número de ítems—, no lo repitas en cada uno: crea un createMemo dentro del Provider y añádelo al valor del contexto como un accesor más. El cálculo ocurre una vez, se cachea, y todos lo comparten. El Provider es el lugar natural para el estado y sus derivaciones.
Alcance de subárbol e instancias propias
Como el store nace dentro del Provider, cada instancia del Provider tiene su propio store. Un CarritoProvider alrededor de toda la app da un carrito global; dos CarritoProvider en dos rincones dan dos carritos independientes. Esto convierte el patrón en algo más versátil que un simple estado global: es estado con alcance, tan amplio o tan acotado como el subárbol que envuelvas. Un asistente por pasos, un panel, una fila editable de una tabla —cada uno puede tener su propio Provider y su propio estado aislado del vecino.
Y como el store cuelga del owner del Provider, su ciclo de vida es automático: cuando el subárbol se desmonta, el owner se dispone, y con él el store y cualquier efecto que abriera. No hay estado que limpiar a mano; la disposición jerárquica del árbol de owners se encarga. Estado compartido, reactivo, con alcance y autolimpieza, en unas veinte líneas.
Store dentro
createStore en el cuerpo del Provider ata el estado al owner de ese subárbol.
Acciones
Cierran sobre setEstado; el consumidor ve verbos de dominio, nunca el setter crudo.
Grano fino
Cada consumidor se actualiza solo en la hoja del store que leyó, sin re-render.
Autolimpieza
Al desmontar el Provider, el owner dispone el store y sus efectos sin código tuyo.
El salto mental que este patrón exige es dejar de ver el estado compartido como un sitio —una variable global, un único árbol de estado— y empezar a verlo como un ámbito —una región del árbol de componentes con su propio estado vivo—. El Provider que crea su store por dentro es, en realidad, una fábrica de estado parametrizada por posición: allí donde lo montes, nace una instancia fresca del estado, con su reactividad de grano fino, sus acciones que encapsulan la mutación y su ciclo de vida atado al owner. Esto tiene tres consecuencias que separan a Solid de casi todo lo demás. Uno: la mutación queda disciplinada sin ceremonia —las acciones cierran sobre setEstado y el consumidor solo ve verbos de dominio, de modo que el “quién puede cambiar esto” tiene una respuesta única y localizada—. Dos: la reactividad es exacta sin que nadie la optimice —el store notifica hoja por hoja, así que un contexto gordo con un estado rico no penaliza a nadie, y cae por tierra la regla de React de trocear contextos para evitar renders, porque aquí no hay renders que evitar—. Y tres: el alcance es libre —el mismo código sirve para un estado global si lo montas en la raíz o para cien estados aislados si lo montas por instancia, y en ambos casos la limpieza es automática porque el owner la gobierna—. Cuando interiorizas que createContext da el dónde y createStore da el qué y el cuándo, dejas de buscar librerías de gestión de estado: el patrón que necesitas son dos primitivos y una plantilla de veinte líneas que ya sabes escribir.
- Implementa
CarritoProviderconcreateStore, las accionesagregar,quitaryvaciar, y el hookuseCarritocon guarda. - Escribe dos consumidores que lean campos distintos del store y comprueba con logs que cada uno se actualiza solo cuando cambia su dato.
- Añade un
createMemodel total dentro delProvidery compártelo por el contexto; verifica que se calcula una vez pese a tener varios lectores. - Monta dos
CarritoProvideren dos zonas de la app y confirma que sus estados son independientes. - Desmonta un
Providery verifica que su store y sus efectos se limpian solos, sin código de limpieza tuyo.