wandres.dev
CONTEXT · createContext, useContext

Múltiples providers, composición y rendimiento

Un contexto por preocupación, providers compuestos sin caer en la pirámide de anidamiento, y el hecho que lo cambia todo respecto a React: en Solid el contexto no provoca re-render, porque no hay render. La lectura es una búsqueda única por owners y la reactividad vive solo en los signals y stores que metes dentro. De ahí se deduce cuándo conviene elevar un Provider y cuándo acotarlo.

⏱ 16 min

Una aplicación real no tiene un contexto, tiene varios: sesión, tema, idioma, carrito, cada uno con su ciclo de vida. Gestionarlos bien es saber tres cosas: separarlos por preocupación, componer sus providers sin ahogar el árbol en anidamiento, y —lo que de verdad distingue a Solid— entender que el contexto no cuesta renders, porque en Solid no hay renders. Ese último hecho invierte por completo los consejos de rendimiento que traes de React y decide, en última instancia, dónde colocar cada Provider.

🎯 Al terminar esta lección sabrás
  • Separar el estado en un contexto por preocupación y componer sus providers.
  • Aplanar la pirámide de providers con un componente agregador o un combinador.
  • Comprender por qué en Solid el contexto no provoca re-render y qué implica.
  • Decidir a qué altura del árbol elevar cada Provider según sus consumidores.

Un contexto por preocupación

La primera regla es de diseño, no de rendimiento: un contexto por eje de estado independiente. La sesión no tiene que ver con el tema, ni el tema con el carrito; mezclarlos en un solo contexto acopla lo que cambia por razones distintas y a ritmos distintos. Cada preocupación crea su propio createContext, su Provider y su hook useX, y se compone con los demás por anidamiento.

function App() {
  return (
    <SesionProvider>
      <TemaProvider>
        <CarritoProvider>
          <Rutas />
        </CarritoProvider>
      </TemaProvider>
    </SesionProvider>
  );
}

Cada Provider envuelve al siguiente, y Rutas —con todo lo que cuelga de ella— consume cualquiera de los tres desde la profundidad que sea. El orden de anidamiento solo importa si un provider necesita consumir a otro: el interno puede leer al externo, nunca al revés.

📝
Un componente no consume su propio Provider

El componente que renderiza un Provider queda por encima de él en el árbol, no por debajo, así que un useContext en su cuerpo no ve el valor que ese mismo componente provee: trepa por owners y pasa de largo. Solo los descendientes del Provider —los children— lo consumen. Si necesitas el valor en el propio componente que lo crea, ya lo tienes en una variable local; el contexto es para repartirlo hacia abajo.

Componer sin la pirámide

Con cuatro o cinco contextos, ese anidamiento degenera en una escalera incómoda —la pirámide de providers—. Se aplana de dos formas. La sencilla: un componente Providers que encapsula la escalera y se usa una vez en la raíz.

const Providers: ParentComponent = (props) => (
  <SesionProvider>
    <TemaProvider>
      <CarritoProvider>{props.children}</CarritoProvider>
    </TemaProvider>
  </SesionProvider>
);

La genérica, cuando la lista crece o se arma dinámicamente: un combinador que pliega un array de providers en uno solo.

import { type ParentComponent, type JSX } from "solid-js";

function combinar(...proveedores: ParentComponent[]): ParentComponent {
  return (props) =>
    proveedores.reduceRight(
      (hijos, Prov) => <Prov>{hijos}</Prov>,
      (<>{props.children}</>) as JSX.Element,
    );
}

const Providers = combinar(SesionProvider, TemaProvider, CarritoProvider);

El reduceRight respeta el orden: el primero de la lista queda más externo. Un combinador así convierte la composición de contextos en datos —una lista— en vez de estructura anidada a mano.

📝
El combinador vale para providers sin props propias

combinar compone providers cuya única entrada son sus children. Si un Provider necesita configuración —<TemaProvider inicial="oscuro">—, o lo dejas fuera del pliegue y lo anidas a mano, o le pasas la configuración por otra vía. Para el caso común de providers autosuficientes, el combinador es perfecto; para los parametrizados, la escalera explícita sigue siendo lo más claro.

El contexto no provoca re-render

Este es el corazón del nivel y el punto donde Solid rompe con React de forma más tajante. En React, cambiar el value de un Provider re-renderiza todos los consumidores de ese contexto, aunque solo les interese una parte; de ahí nace toda una industria de mitigaciones: trocear contextos, memoizar el value, usar selectores. En Solid, nada de eso existe, porque nada se re-renderiza.

La lectura con useContext es una búsqueda única por el árbol de owners que ocurre una vez, cuando el componente se crea. Devuelve una referencia y ahí acaba su papel. La reactividad no la aporta el contexto: la aportan los signals y stores que viajan dentro. Un consumidor se actualiza —de forma quirúrgica— solo cuando cambia la hoja reactiva concreta que leyó, nunca porque “el contexto cambió”.

flowchart TD
P[Provider fija la tupla una vez] --> C1[Consumidor A lee tema]
P --> C2[Consumidor B lee idioma]
S[cambia solo el tema] -.grano fino.-> C1
S -.no lo toca.-> C2
style P fill:#89b4fa,color:#11111b
style C1 fill:#a6e3a1,color:#11111b
style C2 fill:#45475a,color:#cdd6f4

La consecuencia práctica es liberadora: puedes permitirte un contexto “gordo”. Meter un store rico con veinte campos en un solo contexto no penaliza a nadie, porque el grano fino del store notifica campo por campo. El consejo de React de partir contextos para evitar renders no solo es innecesario en Solid: es contraproducente, porque fragmenta sin motivo lo que conceptualmente va junto. En Solid, la granularidad la da el grafo reactivo, no la frontera del contexto.

Esto no te autoriza a meterlo todo en un canal: la regla de una preocupación por contexto sigue siendo buena arquitectura. Lo que cambia es el motivo por el que separas. Ya no divides por miedo al rendimiento —ese impuesto no existe—, sino por cohesión y por ciclo de vida: la sesión y el carrito nacen y mueren en momentos distintos, y mezclarlos en un contexto los ataría sin razón. Un contexto por eje conceptual, cada uno tan gordo como ese eje pida.

ℹ️
Anidar el mismo contexto para sobrescribir un subárbol

Como cada Provider fija su valor por id, anidar dos del mismo contexto hace que el interno gane para su subárbol. Es el mecanismo de las excepciones locales: un tema global oscuro con una sección concreta forzada a claro, un idioma por defecto con un widget en otro. No necesitas un contexto nuevo para una excepción; provees otra vez, más abajo, y ese subárbol ve el valor nuevo mientras el resto sigue con el de arriba.

Cuándo elevar el Provider

Que el contexto no cueste renders no significa subir todo a la raíz. La altura correcta de un Provider es el ancestro común más bajo de sus consumidores. Tres casos cubren casi todo.

🌍

Global: cerca de la raíz

Sesión, tema, idioma —lo que toda la app consulta— va arriba del todo. Una instancia para todos.

🎯

De subárbol: sobre la región

Un asistente, un panel, un formulario complejo: el Provider justo encima, y su estado muere con la región.

♻️

Por instancia: uno por unidad

Cada acordeón, cada fila editable: un Provider por instancia da estados aislados que no se pisan.

El caso “por instancia” es el más revelador, porque el mismo Provider montado varias veces da estados aislados sin una línea extra: la posición en el árbol es la identidad del estado.

function Panel() {
  return (
    <>
      {/* dos acordeones, cada uno con su propio estado abierto/cerrado */}
      <AcordeonProvider><Seccion titulo="Envío" /></AcordeonProvider>
      <AcordeonProvider><Seccion titulo="Pago" /></AcordeonProvider>
    </>
  );
}

El error en un sentido es elevar de más: subir a la raíz un estado que solo usa una pantalla lo mantiene vivo toda la sesión y lo hace único cuando quizá querías uno por instancia. El error opuesto es acotar de menos: si dos primos necesitan el mismo estado y pones el Provider en uno de ellos, el otro no lo alcanza. La brújula es siempre la misma: localiza a todos los consumidores, encuentra su ancestro común más bajo, y pon ahí el Provider. Ni un nivel más arriba —o pierdes aislamiento y limpieza—, ni uno más abajo —o dejas consumidores fuera de alcance—. Que valga la pena usar contexto para ese estado fue la decisión del nivel anterior; este resuelve cómo proveerlo bien una vez decidido que sí.

En Solid el contexto es alcance puro: el coste y la granularidad viven en el grafo, no en la frontera

Todo el instinto que traes de React sobre el contexto hay que recablearlo, y el cable maestro es este: el Provider de Solid no es un punto de re-render, es una anotación de alcance en el árbol de owners. Cambiar lo que provees no despierta a nadie; leerlo no suscribe a nada; la única reactividad que existe es la de los signals y stores que hiciste viajar por dentro, y esa reactividad es de grano fino, hoja por hoja, ajena por completo a la frontera del contexto. De aquí se deducen, sin memorizar reglas, todas las decisiones del nivel. ¿Un contexto gordo o varios finos? Gordo si conceptualmente va junto, porque partir no ahorra renders que no ocurren; la fragmentación de contextos de React es una optimización para un problema que Solid no tiene. ¿Dónde pongo el Provider? En el ancestro común más bajo de sus consumidores, ni más arriba —o sacrificas aislamiento, ciclo de vida y la posibilidad de instancias múltiples— ni más abajo —o dejas huérfano a algún consumidor—. ¿Cómo hago una excepción local? Anidas otro Provider del mismo contexto y su id gana para ese subárbol, sin inventar un canal nuevo. ¿Cómo compongo cinco contextos sin una pirámide? Los pliegas con un combinador, porque son datos, no estructura sagrada. Cuando de verdad interiorizas que el contexto es dónde y el grafo reactivo es cuánto y cuándo, dejas de razonar en términos de renders y de consumidores que se repintan, y empiezas a razonar en términos de alcance y de hojas reactivas —que es como razona Solid por dentro—. Ese cambio de marco es el destilado de todo el nivel: proveer es delimitar un ámbito; reaccionar es cosa del grafo; y separar limpiamente ambas preguntas es lo que te deja construir aplicaciones grandes llenas de contextos sin el menor miedo al rendimiento.

⚔️ Compón y ubica con criterio
  1. Crea tres contextos —sesión, tema, carrito— con sus hooks y compón sus providers primero anidándolos y luego con un componente Providers agregador.
  2. Escribe el combinador combinar con reduceRight y reconstruye Providers a partir de una lista; confirma que el orden externo-interno se respeta.
  3. Mete un store con varios campos en un solo contexto y comprueba, con logs, que un consumidor que lee un campo no se actualiza cuando cambia otro: el contexto gordo no penaliza.
  4. Anida un segundo TemaProvider con valor distinto sobre una sección y verifica que solo ese subárbol cambia de tema.
  5. Toma un estado que hoy está en la raíz pero solo usa una pantalla; bájalo a su ancestro común más bajo y observa que ahora se limpia al salir de esa pantalla.