En 04-01 elevaste el estado al ancestro común y el catálogo de CicloUrbano empezó a filtrar de verdad, pero la lección terminó nombrando el precio de esa técnica: la perforación de props. Cuando el dato vive arriba y se usa abajo, tiene que atravesar todos los componentes intermedios, que lo reciben sin usarlo y lo reenvían sin entenderlo. Con dos niveles molesta; con cinco, cada cambio en la firma de un componente obliga a recorrer media aplicación a mano. useContext es la respuesta de React: un canal directo entre un componente y cualquiera de sus descendientes, sin escalas. En esta lección verás el problema concreto en CicloUrbano, las tres piezas del mecanismo (createContext, el proveedor y useContext), cómo React busca el proveedor más cercano, el patrón profesional de proveedor + hook de acceso aplicado al usuario actual y al tema visual, cómo anidar y sobrescribir proveedores, y —muy importante— cuándo el contexto es la herramienta equivocada.

Un aviso de alcance antes de empezar: aquí estudiamos el mecanismo del contexto y lo aplicamos a dos casos concretos. El contexto como estrategia de gestión del estado de toda la aplicación —el patrón completo, cuándo escala, cuándo no, el rendimiento y la división en varios contextos— es el tema de 07-02, en el módulo dedicado a la gestión del estado. No agotes aquí ese debate: primero hay que dominar la herramienta.

Contenido

  1. El problema: el usuario actual atravesando cuatro niveles
  2. Qué es el contexto y qué no es
  3. Las tres piezas del mecanismo
  4. createContext y el valor por defecto
  5. El proveedor en React 19
  6. useContext y la búsqueda del proveedor más cercano
  7. El patrón profesional: proveedor + hook de acceso
  8. ContextoUsuario completo para CicloUrbano
  9. ContextoTema y el cambio de aspecto
  10. Anidar y sobrescribir proveedores
  11. Cuándo usar contexto y cuándo no
  12. La advertencia de rendimiento

  1. El problema: el usuario actual atravesando cuatro niveles

CicloUrbano necesita mostrar en la cabecera quién ha iniciado sesión, con un menú desplegable. El usuario vive en App, y el menú está cuatro niveles más abajo:

// src/App.jsx
function App() {
  const [usuario, setUsuario] = useState(usuarios[0]);   // usr-01, Ana Ribera
  const [tema, setTema] = useState('claro');

  return (
    <Diseno usuario={usuario} tema={tema} alCambiarTema={setTema}>
      {/* … */}
    </Diseno>
  );
}

// src/componentes/Diseno.jsx — NO usa ninguna de las tres props
function Diseno({ usuario, tema, alCambiarTema, children }) {
  return (
    <div className={estilos.diseno}>
      <Cabecera usuario={usuario} tema={tema} alCambiarTema={alCambiarTema} />
      <main className={estilos.principal}>{children}</main>
      <PieDePagina />
    </div>
  );
}

// src/componentes/Cabecera.jsx — TAMPOCO las usa
function Cabecera({ usuario, tema, alCambiarTema }) {
  return (
    <header className={estilos.cabecera}>
      <h1>CicloUrbano</h1>
      <nav>
        <a href="#catalogo">Catálogo</a>
        <a href="#estaciones">Estaciones</a>
        <a href="#reservas">Mis reservas</a>
      </nav>
      <MenuUsuario usuario={usuario} tema={tema} alCambiarTema={alCambiarTema} />
    </header>
  );
}

// src/componentes/MenuUsuario.jsx — por fin, aquí sí se usan
function MenuUsuario({ usuario, tema, alCambiarTema }) {
  return (
    <div className={estilos.menu}>
      <span>{usuario.nombre}</span>
      {usuario.rol === 'operario' && <a href="#taller">Panel de taller</a>}
      <button type="button" onClick={() => alCambiarTema(tema === 'claro' ? 'oscuro' : 'claro')}>
        Tema {tema === 'claro' ? 'oscuro' : 'claro'}
      </button>
    </div>
  );
}

Visto en el árbol:

flowchart TD
    APP["App<br/><b>estado: usuario, tema</b>"] -->|"usuario, tema, alCambiarTema"| DIS["Diseno<br/>❌ no los usa"]
    DIS -->|"usuario, tema, alCambiarTema"| CAB["Cabecera<br/>❌ no los usa"]
    DIS --> MAIN["main / children"]
    CAB -->|"usuario, tema, alCambiarTema"| MEN["MenuUsuario<br/>✅ AQUÍ se usan"]
    CAB --> NAV["nav"]
    style DIS fill:#fde68a
    style CAB fill:#fde68a
    style MEN fill:#dcfce7

Los dos componentes amarillos son tuberías, no componentes. Y el coste es real:

  • Firmas contaminadas. Diseno declara tres props que no le importan. Quien lea su código tendrá que seguir el rastro para entender de qué van.
  • Cambios en cascada. Si MenuUsuario necesita mañana el idioma, hay que tocar App, Diseno, Cabecera y MenuUsuario. Cuatro ficheros para un dato.
  • Reutilización rota. No puedes usar Diseno en una pantalla que no tenga usuario sin inventarte un valor.
  • Ruido en las pruebas. Probar Cabecera obliga a fabricar un usuario falso aunque la prueba no vaya de eso.

  1. Qué es el contexto y qué no es

El contexto es un mecanismo para que un componente ponga un valor a disposición de todo su subárbol de descendientes, de modo que cualquiera de ellos pueda leerlo directamente, sin recibirlo por props.

Es útil enunciar también lo que no es, porque se malinterpreta con frecuencia:

  • No es un almacén de estado. El contexto transporta un valor; quien lo guarda sigue siendo useState o useReducer en algún componente.
  • No sustituye a las props. Las props siguen siendo la forma normal de pasar datos. El contexto es la excepción para lo «ambiental».
  • No es un canal global. Solo llega a los descendientes del proveedor. Un componente fuera de esa rama no ve nada.
  • No rompe el flujo unidireccional. El dato sigue bajando de ancestro a descendiente; lo único que desaparece son las escalas intermedias.

La imagen mental útil: si las props son un paquete que va de mano en mano por una cadena de personas, el contexto es una megafonía. Quien habla es el proveedor; quien quiera escuchar, escucha; quien esté fuera de la sala, no oye nada.

  1. Las tres piezas del mecanismo

Siempre son las mismas tres, en el mismo orden:

flowchart LR
    A["1. createContext(porDefecto)<br/><i>crea el canal</i>"] --> B["2. &lt;Contexto value={x}&gt;<br/><i>emite el valor</i>"]
    B --> C["3. useContext(Contexto)<br/><i>lo lee, a cualquier profundidad</i>"]
    style A fill:#e0f2fe
    style B fill:#fde68a
    style C fill:#dcfce7
Pieza Dónde vive Qué hace
createContext(valorPorDefecto) En un módulo aparte, fuera de componentes Crea el objeto de contexto. Se importa donde haga falta
El proveedor Lo alto del subárbol que debe ver el valor Emite el valor para todos sus descendientes
useContext(Contexto) En cualquier componente descendiente Lee el valor del proveedor más cercano

  1. createContext y el valor por defecto

// src/contextos/ContextoTema.js
import { createContext } from 'react';

export const ContextoTema = createContext('claro');

createContext se llama una sola vez por contexto, en el ámbito del módulo. Nunca dentro de un componente: se recrearía en cada render y todos los consumidores perderían la conexión.

El argumento es el valor por defecto, y su regla es contraintuitiva: solo se usa cuando un componente llama a useContext y no encuentra ningún proveedor por encima. Si hay proveedor, el valor por defecto es irrelevante, aunque el proveedor emita undefined.

Tienes dos estrategias, y las dos son legítimas:

// Estrategia A: un valor por defecto ÚTIL. El componente funciona aunque falte el proveedor.
export const ContextoTema = createContext('claro');

// Estrategia B: un valor por defecto IMPOSIBLE. Sirve para detectar el fallo.
export const ContextoUsuario = createContext(null);

La estrategia A encaja con datos que tienen un valor razonable por omisión (un tema visual, un idioma). La B encaja con datos cuya ausencia siempre es un error de montaje (el usuario autenticado, un cliente de API): pones null y compruebas en el hook de acceso, como verás en el apartado 7.

  1. El proveedor en React 19

import { ContextoTema } from './contextos/ContextoTema.js';

<ContextoTema value={tema}>
  {/* todo lo que cuelgue de aquí puede leer el tema */}
</ContextoTema>

En React 19 el objeto de contexto se usa directamente como componente. Antes había que escribir <ContextoTema.Provider>, y lo verás en prácticamente todo el código existente y en la documentación de bibliotecas:

// Forma anterior: sigue funcionando en React 19, marcada como obsoleta
<ContextoTema.Provider value={tema}>
  …
</ContextoTema.Provider>
React 18 React 19
Proveer un valor <Contexto.Provider value={x}> <Contexto value={x}>
Consumir con hook useContext(Contexto) useContext(Contexto) (igual)
Consumir sin hook <Contexto.Consumer>{(v) => …}</Contexto.Consumer> Obsoleto: usa el hook

La prop se llama siempre value, esté el resto del proyecto en español o no: es parte de la API de React, como children o key.

Dos precisiones sobre el proveedor:

  • Su alcance es su subárbol de JSX, no su fichero ni su módulo. Lo que no esté dentro de sus children no ve el valor.
  • El valor puede ser cualquier cosa: una cadena, un objeto, una función, o un objeto con datos y funciones a la vez. Es lo normal cuando el subárbol también debe poder cambiar el valor.

  1. useContext y la búsqueda del proveedor más cercano

import { useContext } from 'react';
import { ContextoTema } from '../contextos/ContextoTema.js';

function MenuUsuario() {
  const tema = useContext(ContextoTema);
  …
}

useContext recibe el objeto de contexto, no el proveedor ni el valor. Y hace exactamente esto: sube por el árbol de componentes desde donde se le llama, buscando el primer proveedor de ese mismo contexto.

flowchart TD
    APP["App"] --> PROV["&lt;ContextoTema value='oscuro'&gt;"]
    PROV --> DIS["Diseno"]
    DIS --> CAB["Cabecera"]
    CAB --> MEN["MenuUsuario<br/>useContext(ContextoTema)"]
    MEN -. "busca hacia arriba" .-> CAB
    CAB -. " " .-> DIS
    DIS -. "¡encontrado!" .-> PROV
    PROV -. "devuelve 'oscuro'" .-> MEN
    style PROV fill:#fde68a
    style MEN fill:#dcfce7

Puntos importantes del comportamiento:

  • La búsqueda es hacia arriba, nunca hacia los lados ni hacia abajo. Un componente hermano del proveedor no ve nada.
  • Gana el proveedor más cercano. Si hay dos anidados, el de dentro tapa al de fuera (apartado 10).
  • Si no hay ninguno, se devuelve el valor por defecto de createContext. Este es el caso peligroso: no hay ningún aviso, ningún error, ninguna advertencia en consola. El componente simplemente funciona con datos incorrectos.

Ese último punto es la razón de ser del patrón del apartado siguiente.

  1. El patrón profesional: proveedor + hook de acceso

Usar createContext y useContext sueltos por toda la aplicación tiene tres inconvenientes: cada componente debe importar el contexto, nadie detecta la falta de proveedor, y la lógica de estado acaba repartida. El patrón estándar del ecosistema agrupa todo en un módulo con tres exportaciones: el contexto (a veces privado), un componente proveedor y un hook de acceso.

// Esqueleto del patrón
const Contexto = createContext(null);            // 1. el canal (puede no exportarse)

export function ProveedorX({ children }) {       // 2. el proveedor con el estado dentro
  const [valor, setValor] = useState(inicial);
  return <Contexto value={{ valor, setValor }}>{children}</Contexto>;
}

export function useX() {                          // 3. el hook de acceso con validación
  const contexto = useContext(Contexto);
  if (contexto === null) {
    throw new Error('useX debe usarse dentro de <ProveedorX>');
  }
  return contexto;
}

Las tres ventajas, que compensan de sobra las diez líneas extra:

  • El error de montaje se detecta al instante, con un mensaje que dice exactamente qué falta y dónde. Sin esto, olvidar el proveedor produce un Cannot read properties of null a diez componentes de distancia.
  • Los consumidores no importan el contexto, solo el hook. Si mañana el contexto se divide en dos por rendimiento (07-02), los consumidores no se enteran.
  • El estado vive junto a su proveedor, no disperso en App.

  1. ContextoUsuario completo para CicloUrbano

// src/contextos/ContextoUsuario.jsx
import { createContext, useContext, useState } from 'react';
import { usuarios } from '../datos/dominio.js';

// null como valor por defecto: la ausencia de proveedor SIEMPRE es un error
const ContextoUsuario = createContext(null);

/**
 * Proveedor del usuario que ha iniciado sesión en CicloUrbano.
 * Props:
 *  - children (contenido de la aplicación)
 *  - usuarioInicial (objeto Usuario, opcional, por defecto usr-01)
 */
export function ProveedorUsuario({ children, usuarioInicial = usuarios[0] }) {
  const [usuario, setUsuario] = useState(usuarioInicial);

  function iniciarSesion(id) {
    const encontrado = usuarios.find((candidato) => candidato.id === id);
    if (encontrado) setUsuario(encontrado);
  }

  function cerrarSesion() {
    setUsuario(null);
  }

  const valor = {
    usuario,
    esOperario: usuario?.rol === 'operario',
    iniciarSesion,
    cerrarSesion
  };

  return <ContextoUsuario value={valor}>{children}</ContextoUsuario>;
}

/**
 * Acceso al usuario actual. Lanza si se usa fuera del proveedor.
 */
export function useUsuario() {
  const contexto = useContext(ContextoUsuario);
  if (contexto === null) {
    throw new Error('useUsuario debe usarse dentro de <ProveedorUsuario>');
  }
  return contexto;
}

Detalles del diseño que conviene justificar:

  • El fichero es .jsx, no .js, porque contiene JSX. Y no lleva ñ ni tildes en el nombre, siguiendo la convención del proyecto.
  • ContextoUsuario no se exporta. Nadie de fuera lo necesita: el proveedor lo usa y el hook lo lee. Así es imposible saltarse la validación.
  • esOperario es un valor derivado que se calcula en el proveedor. Evita repetir usuario.rol === 'operario' en cada consumidor, con el riesgo de escribirlo mal.
  • Las funciones viajan dentro del valor. El contexto no solo lleva datos: lleva también la forma de cambiarlos, que es lo que evita seguir pasando alCambiarUsuario por props.

Ahora MenuUsuario se lee sin buscar nada:

// src/componentes/MenuUsuario.jsx
import { useUsuario } from '../contextos/ContextoUsuario.jsx';
import estilos from './MenuUsuario.module.css';

function MenuUsuario() {
  const { usuario, esOperario, cerrarSesion } = useUsuario();

  if (!usuario) {
    return <a href="#acceso" className={estilos.acceso}>Iniciar sesión</a>;
  }

  return (
    <div className={estilos.menu}>
      <span className={estilos.nombre}>{usuario.nombre}</span>
      {esOperario && <a href="#taller">Panel de taller</a>}
      <button type="button" onClick={cerrarSesion}>Cerrar sesión</button>
    </div>
  );
}

export default MenuUsuario;

Y Diseno y Cabecera vuelven a ser lo que eran antes de la perforación:

// src/componentes/Diseno.jsx — sin una sola prop de más
function Diseno({ children }) {
  return (
    <div className={estilos.diseno}>
      <Cabecera />
      <main className={estilos.principal}>{children}</main>
      <PieDePagina />
    </div>
  );
}

El árbol, después:

flowchart TD
    APP["App"] --> PU["&lt;ProveedorUsuario&gt;<br/><b>estado: usuario</b>"]
    PU --> DIS["Diseno<br/>✅ sin props"]
    DIS --> CAB["Cabecera<br/>✅ sin props"]
    CAB --> MEN["MenuUsuario<br/>useUsuario()"]
    PU -. "canal directo" .-> MEN
    style PU fill:#fde68a
    style DIS fill:#dcfce7
    style CAB fill:#dcfce7
    style MEN fill:#dcfce7

Y así queda main.jsx, con el proveedor por encima de App y el límite de error global de 04-05 por encima de todo:

// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import LimiteDeError from './componentes/LimiteDeError.jsx';
import { ProveedorUsuario } from './contextos/ContextoUsuario.jsx';
import { ProveedorTema } from './contextos/ContextoTema.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <LimiteDeError titulo="CicloUrbano no está disponible">
      <ProveedorTema>
        <ProveedorUsuario>
          <App />
        </ProveedorUsuario>
      </ProveedorTema>
    </LimiteDeError>
  </StrictMode>
);

  1. ContextoTema y el cambio de aspecto

El segundo caso canónico. El tema visual lo lee media aplicación y lo cambia un único botón: la definición exacta de dato ambiental.

// src/contextos/ContextoTema.jsx
import { createContext, useContext, useState, useEffect } from 'react';

const ContextoTema = createContext(null);

const TEMAS = ['claro', 'oscuro'];

/**
 * Proveedor del tema visual de CicloUrbano.
 * Props:
 *  - children (contenido)
 *  - temaInicial (cadena, opcional, 'claro' u 'oscuro')
 */
export function ProveedorTema({ children, temaInicial = 'claro' }) {
  const [tema, setTema] = useState(() => {
    const guardado = localStorage.getItem('ciclourbano:tema');
    return TEMAS.includes(guardado) ? guardado : temaInicial;
  });

  // Sincroniza el atributo del <html> y el almacenamiento del navegador (05-02)
  useEffect(() => {
    document.documentElement.dataset.tema = tema;
    localStorage.setItem('ciclourbano:tema', tema);
  }, [tema]);

  function alternarTema() {
    setTema((previo) => (previo === 'claro' ? 'oscuro' : 'claro'));
  }

  return (
    <ContextoTema value={{ tema, esOscuro: tema === 'oscuro', alternarTema }}>
      {children}
    </ContextoTema>
  );
}

export function useTema() {
  const contexto = useContext(ContextoTema);
  if (contexto === null) {
    throw new Error('useTema debe usarse dentro de <ProveedorTema>');
  }
  return contexto;
}

El estado inicial usa inicialización perezosa (05-01) para no leer localStorage en cada render, y el efecto sincroniza con dos sistemas externos (05-02): el atributo data-tema del <html> y el almacenamiento del navegador. Las variables CSS de index.css responden a ese atributo:

/* src/index.css (fragmento) */
:root {
  --color-marca: #12805c;
  --color-fondo: #f5f7fa;
  --color-superficie: #ffffff;
  --color-texto: #1f2933;
  --color-borde: #d9e2ec;
}

:root[data-tema='oscuro'] {
  --color-fondo: #111a22;
  --color-superficie: #1c2733;
  --color-texto: #e6edf3;
  --color-borde: #2d3b48;
}

Fíjate en el reparto de responsabilidades: React gestiona un dato ('claro' u 'oscuro') y el CSS hace todo el trabajo visual mediante variables. Ningún componente necesita useTema para pintarse distinto; solo lo necesita quien tenga que decidir algo en función del tema, como el botón que lo alterna:

// src/componentes/BotonTema.jsx
import { useTema } from '../contextos/ContextoTema.jsx';

function BotonTema() {
  const { esOscuro, alternarTema } = useTema();

  return (
    <button type="button" onClick={alternarTema} aria-pressed={esOscuro}>
      <span aria-hidden="true">{esOscuro ? '☀' : '☾'}</span>
      {esOscuro ? 'Tema claro' : 'Tema oscuro'}
    </button>
  );
}

export default BotonTema;

El aria-pressed viene de 03-06 y de la convención ya establecida en SelectorTipo: un botón que representa un estado activable debe anunciarlo.

  1. Anidar y sobrescribir proveedores

Un mismo contexto puede tener varios proveedores en distintas ramas, o incluso anidados. Gana siempre el más cercano hacia arriba.

function App() {
  return (
    <ProveedorTema temaInicial="claro">
      <Diseno>
        <PanelCatalogo />          {/* lee 'claro' */}

        {/* El panel de taller siempre se muestra en oscuro, sin tocar el global */}
        <ProveedorTema temaInicial="oscuro">
          <PanelTaller />          {/* lee 'oscuro' */}
        </ProveedorTema>
      </Diseno>
    </ProveedorTema>
  );
}
flowchart TD
    PT1["&lt;ProveedorTema 'claro'&gt;"] --> DIS["Diseno"]
    DIS --> CAT["PanelCatalogo<br/>useTema() → claro"]
    DIS --> PT2["&lt;ProveedorTema 'oscuro'&gt;"]
    PT2 --> TAL["PanelTaller<br/>useTema() → oscuro"]
    TAL --> SUB["Subcomponentes<br/>useTema() → oscuro"]
    style PT1 fill:#e0f2fe
    style PT2 fill:#334155,color:#ffffff

Casos donde esto es útil de verdad: una vista previa que debe mostrarse con el tema contrario, una sección con un idioma distinto, o —muy frecuente— las pruebas (Módulo 9), donde envuelves el componente bajo prueba en un proveedor con valores controlados.

Cuando hay varios contextos distintos, se anidan sin más. Si la escalera se vuelve incómoda, un componente que agrupa todos los proveedores lo resuelve:

// src/contextos/Proveedores.jsx
export function Proveedores({ children }) {
  return (
    <ProveedorTema>
      <ProveedorUsuario>
        <ProveedorReservas>{children}</ProveedorReservas>
      </ProveedorUsuario>
    </ProveedorTema>
  );
}

  1. Cuándo usar contexto y cuándo no

El contexto tiene un coste que no se ve en el código: hace implícita una dependencia que antes era explícita. Al leer <MenuUsuario /> ya no sabes de dónde saca sus datos; tienes que abrir el fichero. En un componente que se usa en un solo sitio, eso es peor que una prop.

Situación Herramienta correcta Por qué
El hijo directo necesita un dato del padre Props Explícito, trazable, sin ceremonia
Dos hermanos comparten un dato Elevar el estado (04-01) El ancestro común está a un paso
El dato solo atraviesa uno o dos niveles Props El contexto no compensa
Un componente intermedio solo pasa el dato porque no hay más remedio Composición con children (04-02) Suele eliminar la perforación sin contexto
Muchos componentes de todo el árbol leen el dato y pocos lo cambian Contexto Es exactamente su caso de uso
El dato es «ambiental»: usuario, tema, idioma, permisos, formato de moneda Contexto Ambiental = leído en todas partes
Estado de servidor con caché y revalidación Bibliotecas específicas (07-06) El contexto no cachea ni revalida

La cuarta fila merece un ejemplo, porque mucha gente monta un contexto cuando la composición bastaba. Este Diseno recibe usuario solo para dárselo a la cabecera:

// Perforación por falta de composición
<Diseno usuario={usuario}>
  <PanelCatalogo />
</Diseno>

Con un hueco con nombre (04-02), el dato ya no atraviesa nada:

// src/componentes/Diseno.jsx
function Diseno({ cabecera, children }) {
  return (
    <div className={estilos.diseno}>
      {cabecera}
      <main className={estilos.principal}>{children}</main>
      <PieDePagina />
    </div>
  );
}

// Uso: App crea la cabecera con el usuario y se la entrega ya montada
<Diseno cabecera={<Cabecera usuario={usuario} />}>
  <PanelCatalogo />
</Diseno>

Diseno ya no sabe nada del usuario: recibe un elemento ya construido. Antes de montar un contexto, comprueba si la composición resuelve el caso; es más simple y mantiene las dependencias visibles.

La regla que resume el apartado: el contexto es para datos ambientales que muchos leen y pocos cambian. Cuando el dato es específico de una interacción concreta, las props siguen siendo la respuesta.

  1. La advertencia de rendimiento

Una frase, y la desarrollaremos en su sitio: cuando cambia el valor de un contexto, todos los componentes que lo consumen se vuelven a renderizar, aunque solo usen una parte del valor y esa parte no haya cambiado. Con un tema que se alterna dos veces al día es irrelevante; con un valor que cambia en cada pulsación de tecla, importa.

Las técnicas para gestionarlo —dividir un contexto en varios según su frecuencia de cambio, separar los datos de las funciones que los modifican, y memorizar el valor del proveedor— pertenecen a 07-02 y al Módulo 8. Aquí quédate con la idea de que existe el coste, y con la costumbre de no meter en un mismo contexto cosas que cambian a ritmos muy distintos.

Errores Comunes y Consejos

  • Llamar a createContext dentro de un componente. Se crea un contexto nuevo en cada render y los consumidores dejan de encontrar el proveedor. Va siempre en el ámbito del módulo.
  • Olvidar el proveedor. Sin el patrón del apartado 7 no hay error: el componente recibe el valor por defecto y funciona mal en silencio. Con el hook de acceso que lanza, el fallo aparece en el primer render con un mensaje claro.
  • Creer que el valor por defecto se usa cuando el proveedor emite undefined. No: solo se usa si no hay proveedor en toda la cadena.
  • Proveer desde el componente equivocado. El proveedor debe estar por encima de todos los consumidores. Si lo pones dentro de Cabecera, el catálogo no lo verá.
  • Meter todo el estado de la aplicación en un único contexto. Cualquier cambio repinta a todos los consumidores. Un contexto por asunto.
  • Usar contexto para pasar un dato a un hijo directo. Es complicar una prop. El contexto empieza a compensar a partir de tres niveles, y solo si hay varios consumidores.
  • Exportar el contexto y el hook a la vez sin necesidad. Exportar solo el proveedor y el hook impide que alguien se salte la validación.
  • Consejo: nombra el hook con use + el sustantivo (useUsuario, useTema). Además de la convención, es obligatorio para que las reglas del linter lo traten como hook (04-04).
  • Consejo: en las pruebas, envuelve el componente en su proveedor con valores fijos. Si te resulta difícil, suele ser señal de que el contexto tiene demasiadas responsabilidades.
  • Consejo: pon en el valor del contexto los derivados que usarían varios consumidores (esOperario), no solo los datos crudos. Evita repetir la misma condición por todas partes.

Ejercicios

Ejercicio 1. Este código de CicloUrbano falla en tiempo de ejecución con «Cannot destructure property 'usuario' of null». Encuentra las dos causas y corrígelas.

// src/App.jsx
import { ProveedorUsuario, useUsuario } from './contextos/ContextoUsuario.jsx';

function App() {
  const { usuario } = useUsuario();

  return (
    <ProveedorUsuario>
      <Diseno>
        <p>Bienvenida, {usuario.nombre}</p>
        <PanelCatalogo />
      </Diseno>
    </ProveedorUsuario>
  );
}

Ejercicio 2. Crea ContextoAvisos con el patrón proveedor + hook de acceso. Debe permitir que cualquier componente del árbol muestre un aviso global sin recibir props: el proveedor guarda una lista de avisos { id, tono, texto } (los tonos son los de Aviso: info, exito, advertencia, error), expone mostrarAviso(tono, texto) y descartarAviso(id), y pinta la pila de avisos encima de children. Muestra además cómo lo usaría FormularioReserva al crear una reserva.

Ejercicio 3. TarjetaBicicleta debe mostrar un botón «Enviar a taller» solo si el usuario actual es operario (usr-02, Marc Solé). Hoy recibe usuario por props desde App, atravesando ListaBicicletas. Reescríbelo con useUsuario y explica qué props desaparecen de cada componente de la cadena.

Soluciones

Solución 1.

Las dos causas:

  1. App llama a useUsuario() pero es el propio App quien renderiza <ProveedorUsuario>. Un componente no puede consumir un contexto que él mismo provee: la búsqueda de useContext va hacia arriba, y el proveedor está por debajo de la línea donde se llama al hook. useContext devuelve null (el valor por defecto) y la desestructuración revienta.
  2. usuario.nombre se lee sin comprobar que hay usuario. El proveedor permite cerrarSesion(), que pone usuario a null; ese <p> fallaría en cuanto alguien cerrara sesión.

La corrección mueve el proveedor por encima de App (en main.jsx) y extrae el saludo a un componente descendiente:

// src/main.jsx
createRoot(document.getElementById('root')).render(
  <StrictMode>
    <ProveedorUsuario>
      <App />
    </ProveedorUsuario>
  </StrictMode>
);

// src/App.jsx — ya no llama al hook: solo compone
function App() {
  return (
    <Diseno>
      <Bienvenida />
      <PanelCatalogo />
    </Diseno>
  );
}

// src/componentes/Bienvenida.jsx — descendiente del proveedor: aquí sí
import { useUsuario } from '../contextos/ContextoUsuario.jsx';

function Bienvenida() {
  const { usuario } = useUsuario();

  if (!usuario) return <p>Bienvenido a CicloUrbano. Inicia sesión para reservar.</p>;
  return <p>Bienvenida, {usuario.nombre}</p>;
}

Solución 2.

// src/contextos/ContextoAvisos.jsx
import { createContext, useContext, useState } from 'react';
import Aviso from '../componentes/Aviso.jsx';
import estilos from './ContextoAvisos.module.css';

const ContextoAvisos = createContext(null);

/**
 * Proveedor de la pila de avisos globales de CicloUrbano.
 * Props:
 *  - children (contenido de la aplicación)
 */
export function ProveedorAvisos({ children }) {
  const [avisos, setAvisos] = useState([]);

  function mostrarAviso(tono, texto) {
    const id = `avi-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previos) => [...previos, { id, tono, texto }]);
    return id;
  }

  function descartarAviso(id) {
    setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
  }

  return (
    <ContextoAvisos value={{ avisos, mostrarAviso, descartarAviso }}>
      <div className={estilos.pila} role="status" aria-live="polite">
        {avisos.map((aviso) => (
          <Aviso key={aviso.id} tono={aviso.tono}>
            {aviso.texto}
            <button type="button" onClick={() => descartarAviso(aviso.id)}>
              Descartar
            </button>
          </Aviso>
        ))}
      </div>
      {children}
    </ContextoAvisos>
  );
}

export function useAvisos() {
  const contexto = useContext(ContextoAvisos);
  if (contexto === null) {
    throw new Error('useAvisos debe usarse dentro de <ProveedorAvisos>');
  }
  return contexto;
}

Uso desde FormularioReserva, sin una sola prop nueva en la cadena:

// src/componentes/FormularioReserva.jsx (fragmento)
import { useAvisos } from '../contextos/ContextoAvisos.jsx';

function FormularioReserva({ alCrearReserva, usuarioId = 'usr-01' }) {
  const { mostrarAviso } = useAvisos();

  function manejarEnvio(evento) {
    evento.preventDefault();
    if (Object.keys(errores).length > 0) {
      mostrarAviso('error', 'Revisa los campos marcados antes de continuar.');
      return;
    }
    const reserva = {
      id: `res-${crypto.randomUUID().slice(0, 8)}`,
      bicicletaId: datos.bicicletaId,
      usuario: usuarioId,
      fechaInicio: datos.fechaInicio,
      horas: datos.horas,
      estado: 'activa'
    };
    alCrearReserva(reserva);
    mostrarAviso('exito', `Reserva ${reserva.id} creada para ${reserva.horas} horas.`);
    setDatos(DATOS_INICIALES);
  }
  …
}

Este es el caso de uso ideal del contexto: los avisos son ambientales (cualquier componente puede lanzarlos), se pintan en un único sitio y nadie tiene que enterarse de cómo llegan. Sin contexto, mostrarAviso habría que pasarla como prop desde App a todos los formularios y paneles de la aplicación. Fíjate también en el role="status" con aria-live="polite" de 03-06: los avisos aparecen sin mover el foco, así que hay que anunciarlos.

Solución 3.

// src/componentes/TarjetaBicicleta.jsx
import { useUsuario } from '../contextos/ContextoUsuario.jsx';
import EtiquetaEstado from './EtiquetaEstado.jsx';
import estilos from './TarjetaBicicleta.module.css';

/**
 * Props:
 *  - bicicleta      (objeto Bicicleta, obligatorio)
 *  - nombreEstacion (cadena, opcional)
 *  - alSeleccionar  (función, opcional)
 *  - alReservar     (función, opcional)
 *  - alEnviarATaller (función, opcional): solo se usa si el usuario es operario
 */
function TarjetaBicicleta({ bicicleta, nombreEstacion, alSeleccionar, alReservar, alEnviarATaller }) {
  const { esOperario } = useUsuario();
  const precioFormateado = bicicleta.precioHora.toFixed(2).replace('.', ',');

  return (
    <article className={estilos.tarjeta}>
      <h3>{bicicleta.modelo}</h3>
      <EtiquetaEstado estado={bicicleta.estado} />
      <p>{nombreEstacion} · {precioFormateado} €/h</p>

      <button type="button" onClick={() => alSeleccionar?.(bicicleta)}>Ver detalles</button>
      <button
        type="button"
        onClick={() => alReservar?.(bicicleta)}
        disabled={bicicleta.estado !== 'disponible'}
      >
        Reservar
      </button>

      {esOperario && bicicleta.estado !== 'mantenimiento' && (
        <button type="button" onClick={() => alEnviarATaller?.(bicicleta)}>
          Enviar a taller
        </button>
      )}
    </article>
  );
}

export default TarjetaBicicleta;

Props que desaparecen de la cadena:

Componente Antes Después
App Pasaba usuario a ListaBicicletas Nada: el proveedor está en main.jsx
ListaBicicletas Recibía usuario y lo reenviaba sin usarlo Vuelve a su contrato: bicicletas, estaciones, alSeleccionar, alReservar
TarjetaBicicleta Recibía usuario como prop Lo lee con useUsuario()

Fíjate en lo que no ha cambiado: alEnviarATaller sigue siendo una prop normal. El usuario es un dato ambiental, pero «qué hacer cuando se pulsa este botón concreto de esta tarjeta concreta» es una decisión del padre, y eso es territorio de props. Confundir las dos cosas —meterlo todo en el contexto porque es cómodo— es el error que convierte una aplicación en un ovillo.

Conclusión

La perforación de props que 04-01 dejó pendiente tiene nombre y solución. El contexto es un canal directo entre un ancestro y todos sus descendientes, montado siempre con las mismas tres piezas: createContext(valorPorDefecto) en un módulo aparte, un proveedor que en React 19 se escribe <Contexto value={…}> —con <Contexto.Provider> todavía funcionando en el código que heredes— y useContext(Contexto), que sube por el árbol hasta el proveedor más cercano y, si no encuentra ninguno, devuelve el valor por defecto sin avisar de nada. Por eso el patrón profesional envuelve las tres piezas en un módulo con proveedor + hook de acceso que lanza un error explícito cuando falta el proveedor: así lo has construido en ContextoUsuario y ContextoTema, con Diseno y Cabecera recuperando sus firmas limpias. Sabes anidar proveedores para sobrescribir el valor en una rama, y —lo más importante— sabes cuándo no usarlo: para hijos directos están las props, para hermanos está elevar el estado, y para tuberías está la composición con children. El contexto es para lo ambiental: usuario, tema, idioma, permisos, avisos.

Queda un frente abierto. ProveedorUsuario gestionaba un dato simple, pero el estado de reservas de CicloUrbano no lo es: hay una lista de reservas, un borrador en curso, una fase de envío y un posible error, y todas esas piezas cambian a la vez y con reglas. Con useState acabarías con cinco variables sueltas y manejadores que tocan tres a la vez, justo la situación que 05-01 anunció como límite de la herramienta. React ofrece una alternativa que reúne todas las transiciones en una única función pura, fácil de leer, de probar y de razonar. La próxima lección es Hook useReducer.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados