wandres.dev
CONTEXT · createContext, useContext

useContext y el patrón useX que exige Provider

useContext consume el canal: devuelve el valor del Provider más cercano o el default, trepando por el árbol de owners. Debe leerse en el cuerpo del componente, donde hay owner. Y el patrón profesional no es llamarlo suelto, sino envolverlo en un hook useX que valida que existe Provider y falla con un mensaje claro cuando no.

⏱ 15 min

Proveer un valor sirve de poco sin la otra mitad: consumirlo. useContext es la operación de lectura del canal —entra el objeto de contexto, sale el valor vigente para el punto del árbol donde estás—. Es una línea trivial de escribir y con dos trampas finas: dónde puedes llamarla y qué pasa cuando nadie proveyó nada. La respuesta madura a ambas es un patrón que verás en toda base de código Solid seria: no llamar a useContext a pelo, sino envolverlo en un hook useX que garantiza el Provider.

🎯 Al terminar esta lección sabrás
  • Consumir un contexto con useContext y entender que trepa por owners hasta el proveedor más cercano.
  • Saber dónde es válido llamarlo: en el cuerpo del componente, donde hay owner.
  • Reconocer el agujero del default ausente y el fallo silencioso que produce.
  • Adoptar el patrón useX: un hook que valida el Provider y encapsula el contexto.

useContext: leer el canal

useContext toma el objeto que devolvió createContext y devuelve el valor que corresponde a tu posición en el árbol. Por dentro lee el owner actual y trepa de padre en padre buscando el primer owner que fijó ese id; si lo encuentra, devuelve ese valor; si llega a la raíz sin hallarlo, devuelve el defaultValue.

import { useContext } from "solid-js";

function Boton() {
  const tema = useContext(TemaContext); // "oscuro" si hay Provider, "claro" si no
  return <button classList={{ oscuro: tema === "oscuro" }}>Aceptar</button>;
}

Como el componente corre una sola vez, esta lectura ocurre una vez. No es un coste por render —no hay renders— sino una búsqueda única en el montaje. El resultado lo capturas en una constante y lo usas en el JSX y en los efectos con normalidad.

flowchart BT
HOJA[Boton llama a useContext] --> P1[owner padre sin valor]
P1 --> P2[Provider con el valor]
P2 --> RAIZ[raiz sin proveedor]
P2 -.encontrado devuelve el valor.-> HOJA
style HOJA fill:#a6e3a1,color:#11111b
style P2 fill:#89b4fa,color:#11111b

Dónde se puede llamar: el owner tiene que estar presente

La trampa número uno: useContext necesita un owner actual para saber desde dónde trepar. Ese owner existe mientras se ejecuta el cuerpo del componente y dentro de sus efectos. No existe dentro de un manejador de evento ni en un callback asíncrono suelto: cuando el usuario hace clic, el manejador corre fuera del owner, así que un useContext ahí dentro no trepa por ningún sitio y devuelve el default —casi nunca lo que querías—.

function Perfil() {
  const sesion = useContext(SesionContext); // BIEN: en el cuerpo, hay owner

  const alGuardar = () => {
    // MAL: aqui no hay owner; useContext(SesionContext) devolveria el default
    api.guardar(sesion.usuario); // usa la constante ya leida arriba
  };

  return <button onClick={alGuardar}>Guardar</button>;
}

Lo mismo vale para un setTimeout, un .then o cualquier callback que corra en un tick futuro: para entonces el owner que había durante el montaje ya no es el actual, y useContext no encuentra el canal. La regla operativa es simple: lee el contexto en el cuerpo del componente y cierra sobre el resultado. Nunca dentro del manejador ni de un callback diferido. Es la misma lección que ya viste con la reactividad —el rastreo necesita un contexto de ejecución— aplicada al owner que resuelve el canal.

⚠️
Proveer undefined equivale a no proveer

El buscador interno considera que una entrada con valor undefined es ausencia y sigue trepando. Por eso Provider value={undefined} no hace lo que parece: no apaga el contexto para su subárbol, sino que deja pasar el del proveedor de más arriba o el default. Si necesitas representar un estado vacío, usa un valor centinela explícito —un objeto con un campo null, por ejemplo—, nunca undefined.

El agujero del default ausente

Cuando creas el contexto sin valor por defecto —lo habitual en contextos obligatorios— y un componente lo consume sin Provider encima, useContext devuelve undefined. El desastre no ocurre ahí, sino una línea después, cuando lees una propiedad de ese undefined y la aplicación revienta con un cannot read properties of undefined a kilómetros de la causa real, que era “olvidé envolver en el Provider”.

const SesionContext = createContext<Sesion>(); // sin default -> Sesion | undefined

function Cabecera() {
  const sesion = useContext(SesionContext);
  return <span>{sesion.usuario.nombre}</span>; // si falta el Provider: crash aqui
}

Lo insidioso es la distancia entre causa y síntoma. El undefined no falla al obtenerlo; falla cuando alguien lee un campo suyo, que puede estar varios componentes más allá. El stack trace señala a Cabecera, pero el error real vive en quien la renderizó sin envolverla en el Provider. En una app grande, esa distancia se paga en minutos de depuración por un olvido de una sola línea.

Este es el momento en que el ingeniero con oficio deja de consumir el contexto directamente y construye una guarda.

El patrón useX: un hook que exige el Provider

En lugar de esparcir useContext(SesionContext) por toda la app, se escribe una función useSesion que lo llama, comprueba que hay valor y, si no lo hay, lanza un error explícito. Ese hook pasa a ser la única puerta de entrada al contexto.

export function useSesion() {
  const ctx = useContext(SesionContext);
  if (!ctx) {
    throw new Error("useSesion debe usarse dentro de un SesionProvider");
  }
  return ctx; // a partir de aqui, el tipo ya no incluye undefined
}

Este patrón compra cuatro cosas a la vez. Un fallo temprano y legible: el error apunta al problema real —falta el Provider— en vez de a un síntoma lejano. Encapsulación: puedes no exportar SesionContext y exportar solo useSesion, de modo que el único acceso posible al canal sea el hook, imposibilitando saltarse la validación. Un punto único donde vive el invariante y donde mañana añadirás logging, métricas o valores derivados sin tocar a los consumidores. Y —lo verás en detalle en la próxima lección— estrechamiento de tipo: tras el throw, TypeScript sabe que el valor ya no es undefined.

// El consumidor ya no toca el contexto: solo el hook, sin comprobar nada.
function Avatar() {
  const sesion = useSesion(); // garantizado y no nulo
  return <img src={sesion.usuario.avatar} alt={sesion.usuario.nombre} />;
}
ℹ️
Convención de nombres: el prefijo use

El prefijo use no es magia del framework como en React —Solid no tiene reglas de hooks—, pero comunica de un vistazo el contrato: “esto se llama en el cuerpo de un componente, donde hay owner”. Nombrar useSesion, useTema, useCarrito a estos accesores de contexto documenta su lugar de llamada legítimo sin una sola línea de comentario.

📝
Los hooks de contexto componen

Como useSesion es una función normal, otro hook puede llamarla: un useEsAdmin construido sobre useSesion deriva un booleano sin volver a tocar el contexto. Encapsular el acceso en un hook no solo protege el Provider; abre la puerta a una capa de hooks derivados que expresan reglas de dominio sobre el estado compartido, cada uno apoyado en el anterior.

📖

useContext

Lee el canal trepando por owners; devuelve el Provider más cercano o, si no hay, el defaultValue.

🧭

En el cuerpo

Llámalo donde hay owner. En un manejador de evento devuelve el default: lee arriba y cierra sobre el valor.

🕳️

El agujero

Sin Provider y sin default, devuelve undefined y el crash cae lejos de la causa real.

🚪

useX

Un hook con guarda que exige el Provider, encapsula el contexto y estrecha el tipo.

El hook useX convierte una convención frágil en un invariante que se verifica solo

useContext a secas es correcto pero deja dos deberes en manos de quien lo llama: recordar que solo funciona con un owner presente y recordar que puede devolver el default cuando falta el Provider. Los deberes que dependen de la memoria del programador se incumplen; es cuestión de tiempo. El patrón useX transforma esos deberes en propiedades garantizadas por construcción. Al centralizar el acceso en un único hook, mueves la comprobación del Provider de “algo que cada consumidor debería hacer” a “algo que se hace una vez, en un sitio, y no se puede evitar” —sobre todo si no exportas el objeto de contexto y obligas a pasar por el hook—. El error deja de ser un undefined que estalla lejos y pasa a ser un mensaje que nombra la causa: falta el Provider. Y como bonus estructural, ganas un punto de indirección donde el contrato del contexto puede evolucionar —añadir un valor derivado, instrumentar, versionar— sin que los cien lugares que lo consumen se enteren. Es el mismo principio que rige toda buena API: no expongas el mecanismo, expón la operación con sus invariantes ya garantizados. En Solid este patrón es tan universal que verlo ausente en una base de código es señal de inmadurez: significa que la validación del Provider está dispersa, repetida o —peor— ausente, esperando el día en que alguien renderice un consumidor huérfano y persiga durante una hora un undefined que un hook de tres líneas habría nombrado al instante.

⚔️ Construye la puerta de entrada
  1. Crea un SesionContext sin default y un componente que lo consuma con useContext directo; renderízalo sin Provider y observa el crash y lo lejos que cae de la causa.
  2. Escribe useSesion con la guarda que lanza un error nombrando el Provider ausente; repite y compara la calidad del mensaje.
  3. Deja de exportar SesionContext y exporta solo useSesion; comprueba que los consumidores ya no pueden saltarse la validación.
  4. Llama a useContext dentro de un onClick y verifica que devuelve el default por falta de owner; corrígelo leyendo en el cuerpo y cerrando sobre el valor.
  5. Prueba a proveer value={undefined} y confirma que el subárbol no queda sin contexto, sino que ve el proveedor superior o el default.