En 05-04 aprendiste el mecanismo del contexto y en 05-05 lo combinaste con useReducer, pero las dos lecciones terminaron con la misma advertencia aplazada: «el rendimiento del contexto y su papel como estrategia de estado global se ven en 07-02». Esta es esa lección. Aquí el contexto deja de ser «el truco para no pasar props» y pasa a ser lo que realmente es: una estrategia de gestión de estado con un patrón de uso definido, un coste de rendimiento medible y un límite claro. Vas a ver el patrón completo de un módulo de contexto por dominio, entender de verdad por qué un cambio en un proveedor repinta a todos sus consumidores aunque solo les interese una parte del valor, aplicar las tres soluciones conocidas —dividir contextos, estabilizar el valor y mover el estado hacia abajo—, refactorizar los cuatro contextos de CicloUrbano y componerlos en un único Proveedores, y terminar con la lista honesta de lo que el contexto no resuelve. Esa lista es el puente hacia Redux.

Contenido

  1. Recordatorio de cinco líneas: el mecanismo
  2. El patrón completo de un módulo de contexto
  3. ProveedorAvisos al completo, comentado
  4. El problema de rendimiento, explicado de verdad
  5. Por qué un objeto literal como value cambia en cada render
  6. Solución 1: dividir contextos por frecuencia de cambio
  7. Solución 2: estabilizar el valor con memorización
  8. Solución 3: mover el estado hacia abajo o pasar children
  9. Las tres soluciones comparadas
  10. Refactor de CicloUrbano y el componente Proveedores
  11. Hasta dónde llega el contexto: lo que no resuelve
  12. Contexto + useReducer: el «Redux casero» y en qué se queda corto

  1. Recordatorio de cinco líneas: el mecanismo

Sin repetir 05-04, esto es todo lo que hay que tener presente:

  • createContext(valorPorDefecto) crea un canal; el proveedor <MiContexto value={…}> publica un valor en un subárbol.
  • useContext(MiContexto) busca el proveedor más cercano hacia arriba y devuelve su valor; si no hay ninguno, devuelve el valor por defecto.
  • En React 19 el propio contexto se usa como proveedor: <ContextoTema value={valor}>, sin .Provider.
  • El contexto no es estado: es un mecanismo de transporte. El estado sigue estando en un useState o un useReducer dentro del proveedor.
  • Cuando el valor publicado cambia, todos los componentes que hacen useContext de ese contexto se vuelven a ejecutar.

Esa última línea es la que se desarrolla en esta lección, porque contiene todo el coste.

  1. El patrón completo de un módulo de contexto

Un contexto bien hecho no es una llamada a createContext suelta: es un módulo por dominio con cinco piezas siempre en el mismo orden. Es la estructura que ya siguen ContextoUsuario y ContextoTema, formalizada.

Pieza Qué es Regla
El contexto const ContextoX = createContext(null) No se exporta. Así nadie puede saltarse el hook de acceso
El estado useState o useReducer dentro del proveedor Es el dueño real del dato
El valor expuesto El objeto que se publica Se decide deliberadamente: datos, derivados y acciones
El proveedor export function ProveedorX({ children }) Componente propio, en su fichero, con su CSS si lo necesita
El hook de acceso export function useX() Lanza si el contexto es null: falta el proveedor
// src/contextos/ContextoEjemplo.jsx — el esqueleto del patrón
import { createContext, useContext, useState } from 'react';

// 1) El contexto: privado del módulo
const ContextoEjemplo = createContext(null);

// 2) y 3) y 4) El proveedor: dueño del estado y del valor
export function ProveedorEjemplo({ children }) {
  const [valor, setValor] = useState(null);

  const contenido = { valor, establecerValor: setValor };

  return <ContextoEjemplo value={contenido}>{children}</ContextoEjemplo>;
}

// 5) El hook de acceso: única puerta de entrada
export function useEjemplo() {
  const contexto = useContext(ContextoEjemplo);
  if (contexto === null) {
    throw new Error('useEjemplo debe usarse dentro de <ProveedorEjemplo>');
  }
  return contexto;
}

Por qué el hook que lanza importa más de lo que parece: sin él, un componente colocado fuera del proveedor recibiría el valor por defecto —normalmente null— y fallaría más tarde con un Cannot read properties of null, en otro punto del código y con un mensaje que no dice nada. Con el throw, el fallo aparece exactamente donde está el error y con el nombre del proveedor que falta.

Un fichero por dominio. No un ContextoGlobal.jsx con la sesión, el tema y los avisos juntos: eso es un único proveedor cuyo valor cambia cada vez que cambia cualquiera de los tres, y es el origen del problema del apartado 4.

  1. ProveedorAvisos al completo, comentado

Este es el módulo de avisos de CicloUrbano escrito con el patrón entero. Aparecía esbozado en 05-04; aquí está la versión que se queda en el proyecto.

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

const ContextoAvisos = createContext(null);

const DURACION_POR_DEFECTO = 5000;

export function ProveedorAvisos({ children }) {
  // El estado real: una pila de { id, tono, texto }
  const [avisos, setAvisos] = useState([]);

  // useCallback mantiene la identidad de la función entre renders.
  // El detalle de por qué funciona está en 08-03; aquí basta con saber
  // que sin él estas funciones serían nuevas en cada render.
  const descartarAviso = useCallback((id) => {
    setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
  }, []);

  const mostrarAviso = useCallback((tono, texto, duracion = DURACION_POR_DEFECTO) => {
    const id = `aviso-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previos) => [...previos, { id, tono, texto }]);

    if (duracion > 0) {
      setTimeout(() => {
        setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
      }, duracion);
    }
    return id;
  }, []);

  const valor = { avisos, mostrarAviso, descartarAviso };

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

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

Decisiones que conviene justificar:

  • setAvisos con función actualizadora en las tres llamadas. Es imprescindible: un setTimeout que se dispara cinco segundos después no puede fiarse de la avisos capturada en aquel render (05-01).
  • El identificador se genera en el proveedor, no lo pasa quien llama. Quien muestra un aviso no debería preocuparse de eso.
  • La pila se pinta dentro del proveedor, después de {children}. Así cualquier componente del árbol puede lanzar avisos y hay un único sitio donde se pintan.
  • role="status" con aria-live="polite" (03-06): los avisos aparecen sin mover el foco, así que deben anunciarse a los lectores de pantalla.
  • useCallback en las dos acciones. Es la primera pieza de la solución al problema de rendimiento y el motivo de que en el apartado 6 se pueda separar el contexto de acciones.

  1. El problema de rendimiento, explicado de verdad

Aquí está el coste real del contexto, y conviene enunciarlo con precisión porque se repite mal con frecuencia.

Cuando el valor de un proveedor cambia, React vuelve a ejecutar todos los componentes que consumen ese contexto, aunque solo les interese una parte del valor que no ha cambiado.

No hay selección granular. useContext es «suscríbeme al valor entero». Si el valor es { avisos, mostrarAviso, descartarAviso } y solo cambia avisos, un componente que únicamente usa mostrarAviso se vuelve a ejecutar igualmente.

Veamos el caso concreto de CicloUrbano. ContextoReservas publica { estado, despachar }, y estado contiene reservas, borrador, estadoEnvio y error. El borrador cambia en cada pulsación de teclado del formulario de reserva.

flowchart TD
    A["El usuario escribe una letra<br/>en FormularioReserva"] --> B["despachar borrador_actualizado"]
    B --> C["reductorReservas devuelve<br/>un estado nuevo"]
    C --> D["ProveedorReservas se ejecuta<br/>y publica un valor nuevo"]
    D --> E["PanelReservas<br/>usa reservas"]
    D --> F["Cabecera<br/>usa el contador de reservas"]
    D --> G["PaginaReservas<br/>usa reservas"]
    D --> H["FormularioReserva<br/>usa borrador"]
    D --> I["DialogoReserva<br/>solo usa despachar"]
    E --> J["Repintado innecesario"]
    F --> J
    G --> J
    I --> J
    H --> K["Repintado necesario"]
    style J fill:#fde68a,stroke:#b45309
    style K fill:#bbf7d0,stroke:#12805c

Cuatro de los cinco consumidores se han vuelto a ejecutar sin que nada de lo que leen haya cambiado. Multiplícalo por las letras de «Eléctrica Pro» y tienes trece rondas de repintado de media aplicación por escribir un modelo.

Un matiz importante para no exagerar el problema: volver a ejecutar un componente no es lo mismo que tocar el DOM. React ejecuta la función, compara el resultado con el anterior y solo aplica al DOM lo que ha cambiado (01-05). Si los componentes son baratos, no notarás nada. El problema aparece cuando los consumidores son caros —listas largas, cálculos, subárboles grandes— o cuando el valor cambia muy a menudo. Y en el catálogo de CicloUrbano, con ListaBicicletas pintando tarjetas, se cumplen las dos condiciones.

  1. Por qué un objeto literal como value cambia en cada render

Hay un segundo problema, más sutil y muy fácil de sufrir sin darse cuenta: el valor puede «cambiar» sin que cambie el estado.

export function ProveedorUsuario({ children }) {
  const [usuario, setUsuario] = useState(null);

  function iniciarSesion(id) { /* … */ }
  function cerrarSesion() { setUsuario(null); }

  // ⚠️ Objeto literal nuevo en CADA render del proveedor
  const valor = {
    usuario,
    esOperario: usuario?.rol === 'operario',
    iniciarSesion,
    cerrarSesion
  };

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

React decide si debe avisar a los consumidores comparando el valor nuevo con el anterior mediante Object.is, es decir, por identidad de referencia. Y { usuario, … } construye un objeto nuevo cada vez que se ejecuta la función. Aunque usuario sea exactamente el mismo objeto, valor es una referencia distinta y todos los consumidores se repintan.

Lo mismo ocurre con iniciarSesion y cerrarSesion: son funciones redeclaradas en cada render, con identidad nueva cada vez.

¿Cuándo se re-ejecuta el proveedor? Siempre que se re-ejecuta su padre. Y aquí está la trampa: si ProveedorUsuario está dentro de un componente que se repinta por cualquier motivo ajeno —un cambio de ruta, un cambio de tema—, publicará un valor nuevo, y todos los consumidores de la sesión se repintarán sin que la sesión haya cambiado en absoluto.

// Comparación de identidades entre dos renders del proveedor
// Render 1: valor === { usuario: usr01, esOperario: false, … }   ← referencia A
// Render 2: valor === { usuario: usr01, esOperario: false, … }   ← referencia B
// Object.is(A, B) → false  ⇒  React avisa a todos los consumidores

Este es el fallo que arregla la solución 2. Pero antes, la que más rendimiento da por menos código.

  1. Solución 1: dividir contextos por frecuencia de cambio

La idea es sencilla y muy eficaz: si un valor tiene partes que cambian a ritmos distintos, publícalas en contextos distintos. Cada consumidor se suscribe solo a lo que necesita.

En ContextoReservas hay una división evidente y gratuita: estado cambia constantemente, mientras que despachar es estable por garantía de React —lo viste en 05-05: useReducer devuelve siempre la misma función despachar durante toda la vida del componente—. Separarlos convierte a todos los componentes que solo despachan en inmunes a los cambios de estado.

// src/contextos/ContextoReservas.jsx
import { createContext, useContext, useReducer } from 'react';
import { reductorReservas, ESTADO_INICIAL_RESERVAS } from '../reductores/reservas.js';

// Dos contextos: uno para lo que cambia y otro para lo que no
const ContextoReservasEstado = createContext(null);
const ContextoReservasAcciones = createContext(null);

export function ProveedorReservas({ children }) {
  const [estado, despachar] = useReducer(reductorReservas, ESTADO_INICIAL_RESERVAS);

  return (
    // despachar es ESTABLE: este proveedor nunca publica un valor nuevo
    <ContextoReservasAcciones value={despachar}>
      <ContextoReservasEstado value={estado}>
        {children}
      </ContextoReservasEstado>
    </ContextoReservasAcciones>
  );
}

/** Lee el estado de reservas. Se repinta cuando el estado cambia. */
export function useReservasEstado() {
  const contexto = useContext(ContextoReservasEstado);
  if (contexto === null) {
    throw new Error('useReservasEstado debe usarse dentro de <ProveedorReservas>');
  }
  return contexto;
}

/** Obtiene despachar. NUNCA provoca un repintado por cambio de estado. */
export function useReservasAcciones() {
  const contexto = useContext(ContextoReservasAcciones);
  if (contexto === null) {
    throw new Error('useReservasAcciones debe usarse dentro de <ProveedorReservas>');
  }
  return contexto;
}

Puntos clave de esta versión:

  • El contexto de acciones publica despachar directamente, no { despachar }. Un objeto envolvente volvería a introducir el problema de identidad del apartado 5, precisamente el que estamos evitando.
  • El orden de anidamiento importa poco funcionalmente, pero el de acciones va fuera por claridad: es el que nunca cambia.
  • Se mantiene el hook que lanza, ahora duplicado. Es repetición aceptable: cada hook documenta su coste en su propio nombre.

El efecto sobre el diagrama del apartado 4 es directo: DialogoReserva, que solo despacha, deja de repintarse al escribir en el formulario. Y en un componente que despacha desde un manejador —el caso más común— la mejora se multiplica por cuantos consumidores «solo escritores» tenga la aplicación.

La división también aplica por dominio, no solo por estado/acciones. Si hoy tuvieras un único ContextoGlobal con sesión, tema y avisos, cada aviso repintaría a los lectores de la sesión. Que CicloUrbano tenga cuatro contextos separados desde 05-04 ya es, de hecho, una aplicación de esta regla.

Compatibilidad con lo que ya está escrito. Si prefieres no tocar los consumidores actuales de golpe, mantén un useReservas() de conveniencia mientras dure la migración:

/** Compatibilidad: devuelve { estado, despachar } como antes.
 *  Se suscribe a los DOS contextos, así que no ahorra repintados.
 *  Úsalo solo mientras migras los componentes. */
export function useReservas() {
  return { estado: useReservasEstado(), despachar: useReservasAcciones() };
}

Es honesto reconocer lo que hace: no optimiza nada. Sirve para migrar por partes, y debería desaparecer cuando el último componente use el hook granular.

  1. Solución 2: estabilizar el valor con memorización

La segunda solución ataca el problema del apartado 5: evitar que el valor cambie de identidad cuando su contenido no ha cambiado.

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

const ContextoUsuario = createContext(null);

export function ProveedorUsuario({ children }) {
  const [usuario, setUsuario] = useState(null);
  const [cargandoSesion, setCargandoSesion] = useState(true);

  // Identidad estable: no se redeclaran en cada render
  const iniciarSesion = useCallback((id) => {
    const encontrado = usuarios.find((candidato) => candidato.id === id);
    if (encontrado) setUsuario(encontrado);
  }, []);

  const cerrarSesion = useCallback(() => setUsuario(null), []);

  // El objeto solo se reconstruye si cambia usuario o cargandoSesion
  const valor = useMemo(
    () => ({
      usuario,
      esOperario: usuario?.rol === 'operario',
      cargandoSesion,
      iniciarSesion,
      cerrarSesion
    }),
    [usuario, cargandoSesion, iniciarSesion, cerrarSesion]
  );

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

Lo que consigue: si ProveedorUsuario se vuelve a ejecutar por un motivo ajeno a la sesión, useMemo devuelve el mismo objeto de antes, Object.is da true y React no avisa a ningún consumidor.

Lo que no consigue: si la sesión sí cambia, todos los consumidores se repintan igual, incluidos los que solo usan cerrarSesion. Estabilizar evita los repintados espurios; no da selección granular. Para eso está la solución 1.

useMemo y useCallback se estudian a fondo en 08-03: qué memorizan exactamente, cuándo compensan y cuándo son ruido. Aquí basta con la regla operativa:

Todo proveedor que publique un objeto literal como value debe estabilizarlo con useMemo, y las funciones que ese objeto contenga con useCallback. Es de los pocos sitios donde memorizar es la opción por defecto y no una optimización prematura.

  1. Solución 3: mover el estado hacia abajo o pasar children

La tercera solución no toca el contexto: cambia la estructura para que menos componentes dependan de él.

8.1 Mover el estado hacia abajo

Si un estado que vive en un proveedor lo usa en realidad una zona pequeña del árbol, bájalo. Es el «bajar» del apartado 5 de 07-01, aplicado a un contexto.

// ANTES: el término de búsqueda en un contexto que lee toda la aplicación
// DESPUÉS: en el componente que lo usa
function BuscadorBicicletas({ alBuscar }) {
  const [termino, setTermino] = useState('');
  const terminoRetrasado = useDebounce(termino, 400);
  // …
}

Si el dato no necesita estar arriba, sacarlo del contexto elimina el problema en lugar de mitigarlo. Es la única de las tres soluciones que reduce complejidad en vez de añadirla.

8.2 Pasar children para aislar subárboles

Este truco es menos conocido y muy potente. Un componente que se vuelve a ejecutar no fuerza a re-ejecutar los hijos que ha recibido como children, porque esos elementos ya venían creados desde fuera: su identidad no ha cambiado.

// ANTES: el proveedor construye su contenido, así que todo se re-ejecuta con él
function ProveedorTema() {
  const [tema, setTema] = useState('claro');
  return (
    <ContextoTema value={{ tema, setTema }}>
      <Cabecera />
      <Catalogo />   {/* se re-ejecuta con cada cambio de tema, lo use o no */}
    </ContextoTema>
  );
}

// DESPUÉS: el contenido llega desde fuera y no se re-ejecuta al cambiar el tema
function ProveedorTema({ children }) {
  const [tema, setTema] = useState('claro');
  const valor = useMemo(() => ({ tema, setTema }), [tema]);
  return <ContextoTema value={valor}>{children}</ContextoTema>;
}

Con la segunda versión, un cambio de tema re-ejecuta ProveedorTema, pero children es el mismo elemento de React que ya existía: React lo salta y solo se re-ejecutan los componentes que realmente consumen ContextoTema. Es la razón técnica por la que un proveedor debe recibir siempre children y no construir su contenido, y por la que el patrón del apartado 2 lo exige.

  1. Las tres soluciones comparadas

1. Dividir contextos 2. Estabilizar con memorización 3. Bajar el estado / children
Qué problema resuelve Consumidores repintados por partes del valor que no usan Repintados espurios por identidad nueva sin cambio real Que haya consumidores de más
Esfuerzo Medio: dos contextos y dos hooks por dominio Bajo: useMemo + useCallback en el proveedor Variable: puede exigir reestructurar
Ganancia Alta cuando hay muchos «solo escritores» Media: elimina el ruido, no la cascada real La mayor: el problema desaparece
Riesgo Más ficheros y más hooks que recordar Dependencias mal declaradas en useMemo Reintroducir prop drilling si te pasas
Cuándo usarla Estado que cambia a menudo + acciones estables Siempre en cualquier proveedor con objeto literal Cuando el dato no necesitaba estar arriba

No son excluyentes: en CicloUrbano se aplican las tres a la vez. La 2 es obligatoria en todos los proveedores, la 1 solo en los que tienen un valor de ritmos mixtos, y la 3 es la revisión que se hace al clasificar el estado (07-01).

  1. Refactor de CicloUrbano y el componente Proveedores

Con las reglas aplicadas, así queda el mapa de contextos.

Contexto Estado Frecuencia de cambio Tratamiento
ContextoTema tema Muy baja useMemo en el valor. No hace falta dividir
ContextoUsuario usuario, cargandoSesion Muy baja useMemo + useCallback. No hace falta dividir
ContextoAvisos avisos Media useCallback en las acciones + división estado/acciones
ContextoReservas Estado del reductor Alta (cada tecla) División obligatoria: estado y despachar separados

Y el problema que queda: main.jsx y Diseno acumulan una pirámide de proveedores anidados que crece con cada dominio nuevo. La solución es un componente de composición.

// src/contextos/Proveedores.jsx
import { ProveedorTema } from './ContextoTema.jsx';
import { ProveedorUsuario } from './ContextoUsuario.jsx';

/**
 * Proveedores globales de CicloUrbano, en su orden correcto.
 * Los que dependen del enrutador (avisos, reservas) viven en Diseno.
 */
export function Proveedores({ children }) {
  return (
    <ProveedorTema>
      <ProveedorUsuario>
        {children}
      </ProveedorUsuario>
    </ProveedorTema>
  );
}
// src/main.jsx — con la composición aplicada
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router';
import { router } from './rutas.jsx';
import LimiteDeError from './componentes/LimiteDeError.jsx';
import { Proveedores } from './contextos/Proveedores.jsx';
import { registrarError } from './utilidades/monitorizacion.js';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <LimiteDeError titulo="CicloUrbano no está disponible ahora mismo" alRegistrar={registrarError}>
      <Proveedores>
        <RouterProvider router={router} />
      </Proveedores>
    </LimiteDeError>
  </StrictMode>
);
flowchart TD
    A["StrictMode"] --> B["LimiteDeError"]
    B --> C["Proveedores<br/>(Tema + Usuario)"]
    C --> D["RouterProvider"]
    D --> E["Diseno (ruta raíz)"]
    E --> F["ProveedorReservas<br/>estado + acciones separados"]
    F --> G["ProveedorAvisos"]
    G --> H["Cabecera · Outlet · PieDePagina"]

Dos reglas de colocación que ya conocías de 06-02 y 06-03 y que la composición no debe romper:

  • ProveedorTema y ProveedorUsuario van fuera del enrutador, porque deben sobrevivir a absolutamente todo, incluida la pantalla de error de ruta.
  • ProveedorReservas y ProveedorAvisos van dentro de Diseno, la ruta raíz, porque pertenecen al marco de la aplicación y Diseno no se desmonta al navegar.

El orden de anidamiento tiene una regla adicional: si un proveedor consume otro, va por dentro. ProveedorReservas podría necesitar el usuario para asociar la reserva; el usuario, en cambio, no necesita nada de las reservas. Por eso el usuario va fuera.

  1. Hasta dónde llega el contexto: lo que no resuelve

El contexto es una herramienta excelente para lo que hace, y no hay que sustituirlo por costumbre. Pero tiene un techo, y estas son las cinco cosas concretas que no aporta.

Carencia Qué significa en la práctica
Herramientas de depuración No hay un panel que te enseñe qué cambió, cuándo y quién lo provocó. La depuración es console.log y el Profiler
Middleware No hay un punto único donde interceptar todos los cambios para registrar, medir, persistir o comprobar permisos. Hay que repetirlo en cada proveedor
Viaje en el tiempo No puedes retroceder al estado anterior para reproducir un fallo. Ni deshacer/rehacer sin implementarlo entero
Selección granular useContext te suscribe al valor entero. Dividir contextos mitiga, no resuelve: no existe «suscríbeme solo a estado.reservas»
Lógica asíncrona organizada Cada proveedor resuelve sus peticiones a su manera. No hay convención compartida para carga, error y cancelación

Las señales de que ha llegado el momento de plantearse una biblioteca dedicada:

  1. Ya has dividido los contextos y sigues viendo repintados que no deberían ocurrir.
  2. Tienes más de seis u ocho proveedores y el orden de anidamiento entre ellos empieza a importar de formas sutiles.
  3. Necesitas saber qué provocó un cambio de estado, y console.log en el reductor se te ha quedado corto.
  4. Varios proveedores necesitan reaccionar al mismo suceso —«se cerró la sesión, limpia reservas, avisos y borradores»— y estás pasando funciones de un proveedor a otro.
  5. El equipo ha crecido y hace falta una convención común, no que cada dominio invente su patrón.
  6. Quieres persistir parte del estado, o registrar cada cambio para diagnóstico, y no quieres escribirlo cuatro veces.

Si no te reconoces en ninguna, el contexto te está bastando y añadir Redux sería globalización prematura (07-01). Si te reconoces en tres o más, sigue leyendo el módulo.

  1. Contexto + useReducer: el «Redux casero» y en qué se queda corto

En 05-05 se dijo que el modelo de Redux es exactamente el de useReducer, y es literalmente cierto. Con useReducer + contexto ya tienes:

  • Un estado centralizado por dominio (el del reductor).
  • Cambios solo por acciones descriptivas, no por asignaciones sueltas.
  • Un reductor puro que concentra todas las transiciones en un sitio y se prueba sin React.
  • Un despachar estable disponible en todo el árbol.

Esos son los tres principios de Redux, que verás enunciados en la próxima lección. La diferencia no es conceptual, es de infraestructura:

Contexto + useReducer Redux Toolkit
Modelo mental Idéntico Idéntico
Alcance Un reductor por dominio, cada uno en su proveedor Un almacén único con varios reductores combinados
Suscripción Al valor entero del contexto Por selector, con comparación por identidad
Depuración Manual DevTools con historial y viaje en el tiempo
Interceptar cambios No hay punto único Middleware
Asincronía Cada quien la resuelve como puede createAsyncThunk con un patrón común
Vive fuera de React No: el estado está en el árbol Sí: el almacén es un objeto independiente
Coste Cero dependencias Una dependencia y un vocabulario

La última fila de la comparación es más importante de lo que parece. En contexto + useReducer, el estado vive dentro del árbol de React: si el proveedor se desmonta, el estado desaparece, y nada que no sea un componente puede leerlo. El almacén de Redux es un objeto JavaScript normal que existe al margen de React; puede leerse desde una utilidad, desde un interceptor de peticiones o desde una prueba, sin montar nada.

Dicho lo cual, y para cerrar con honestidad: contexto + useReducer es una solución perfectamente válida para muchísimas aplicaciones, y CicloUrbano funcionaría bien así indefinidamente. Lo que sigue en el módulo no es una corrección de un error: es lo que ganas cuando la aplicación y el equipo crecen.

Errores Comunes y Consejos

Error 1: publicar un objeto literal sin useMemo. Es el fallo más extendido y el más silencioso, porque la aplicación funciona: solo va más lenta de lo que debería y nadie sabe por qué. Regla fija: objeto literal en valueuseMemo.

Error 2: un único ContextoGlobal para todo. Junta datos con frecuencias de cambio incompatibles y garantiza que un aviso repinte a los lectores de la sesión. Un fichero por dominio.

Error 3: exportar el contexto además del hook. En cuanto está exportado, alguien lo usará con useContext directamente y se saltará la validación del proveedor. Mantenlo privado del módulo.

Error 4: que el proveedor construya su contenido en vez de recibir children. Anula el aislamiento del apartado 8.2 y hace que todo el subárbol se re-ejecute con cada cambio del proveedor.

Error 5: envolver despachar en un objeto. value={{ despachar }} crea una referencia nueva en cada render y tira por la borda la estabilidad que React te regalaba. Publícalo tal cual.

Error 6: creer que dividir contextos da selección granular. Divide por ritmos de cambio, no en cien contextos de un campo cada uno. Si necesitas de verdad suscribirte a un campo concreto de un objeto grande, esa es exactamente la señal 1 del apartado 11.

Consejo 1: nombra los hooks por su coste. useReservasEstado y useReservasAcciones dicen en su nombre a qué te suscribes. Quien lee el componente sabe si va a repintarse.

Consejo 2: comprueba el efecto con el Profiler. Antes y después de dividir un contexto, graba con React DevTools Profiler (08-05) y compara. Es la única forma de saber si el refactor sirvió de algo.

Consejo 3: no dividas por adelantado. Empieza con un contexto por dominio y el valor memorizado. Divide en estado y acciones cuando el estado empiece a cambiar a menudo, no antes.

Ejercicios

Ejercicio 1. Este proveedor tiene cuatro problemas de los vistos en la lección. Identifícalos y reescríbelo aplicando el patrón completo.

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

export const ContextoTema = createContext('claro');

export function ProveedorTema() {
  const [tema, setTema] = useState('claro');

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

  return (
    <ContextoTema value={{ tema, alternarTema }}>
      <Cabecera />
      <Catalogo />
      <PieDePagina />
    </ContextoTema>
  );
}

Ejercicio 2. Divide ContextoAvisos en estado y acciones siguiendo el patrón de ContextoReservas, y explica qué componentes de CicloUrbano ganan con la división. Ten en cuenta que mostrarAviso y descartarAviso son dos funciones, no una: piensa cómo publicar el contexto de acciones sin reintroducir el problema de identidad.

Ejercicio 3. En CicloUrbano, Cabecera muestra un distintivo con el número de reservas activas y MenuUsuario muestra el nombre del usuario. Con la implementación actual, escribir una letra en FormularioReserva repinta las dos. Explica por qué ocurre con cada una y propón la solución concreta para cada caso.

Soluciones

Solución 1. Los cuatro problemas:

  1. El contexto está exportado (export const ContextoTema), así que cualquiera puede usarlo con useContext saltándose la validación.
  2. No hay hook de acceso que lance si falta el proveedor. Y el valor por defecto es 'claro', una cadena, mientras que el valor real es un objeto: quien lo use fuera del proveedor recibirá 'claro' y fallará al hacer tema.tema, con un error incomprensible.
  3. El proveedor construye su contenido en lugar de recibir children, así que cada cambio de tema re-ejecuta Cabecera, Catalogo y PieDePagina aunque no consuman el contexto.
  4. El valor es un objeto literal sin memorizar, y alternarTema se redeclara en cada render. Además alternarTema lee tema de la clausura en vez de usar la función actualizadora.
// src/contextos/ContextoTema.jsx — corregido
import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';

const ContextoTema = createContext(null);   // 1) privado del módulo

export function ProveedorTema({ children }) {   // 3) recibe children
  const [tema, setTema] = useState('claro');

  // 4) identidad estable + función actualizadora
  const alternarTema = useCallback(() => {
    setTema((previo) => (previo === 'claro' ? 'oscuro' : 'claro'));
  }, []);

  // Sincroniza el atributo que usan las variables CSS de index.css
  useEffect(() => {
    document.documentElement.dataset.tema = tema;
  }, [tema]);

  // 4) valor estabilizado
  const valor = useMemo(() => ({ tema, alternarTema }), [tema, alternarTema]);

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

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

Solución 2. La clave está en el enunciado: como hay dos funciones, publicarlas como { mostrarAviso, descartarAviso } crearía un objeto nuevo en cada render. Hay que memorizar también ese objeto de acciones, que ahora sí es estable de verdad porque sus dos miembros llevan useCallback con dependencias vacías.

// src/contextos/ContextoAvisos.jsx — dividido
const ContextoAvisosEstado = createContext(null);
const ContextoAvisosAcciones = createContext(null);

export function ProveedorAvisos({ children }) {
  const [avisos, setAvisos] = useState([]);

  const descartarAviso = useCallback((id) => {
    setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
  }, []);

  const mostrarAviso = useCallback((tono, texto, duracion = DURACION_POR_DEFECTO) => {
    const id = `aviso-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previos) => [...previos, { id, tono, texto }]);
    if (duracion > 0) {
      setTimeout(() => {
        setAvisos((previos) => previos.filter((aviso) => aviso.id !== id));
      }, duracion);
    }
    return id;
  }, []);

  // Objeto memorizado con [] efectivas: nunca cambia de identidad
  const acciones = useMemo(
    () => ({ mostrarAviso, descartarAviso }),
    [mostrarAviso, descartarAviso]
  );

  return (
    <ContextoAvisosAcciones value={acciones}>
      <ContextoAvisosEstado value={avisos}>
        {children}
        <ListaAvisos />
      </ContextoAvisosEstado>
    </ContextoAvisosAcciones>
  );
}

export function useAvisosEstado() { /* … lanza si es null … */ }
export function useAvisosAcciones() { /* … lanza si es null … */ }

Quién gana con la división: todos los componentes que solo lanzan avisos y nunca los leen, que en CicloUrbano son casi todos —FormularioReserva, PaginaNuevaReserva, PanelReservas, PaginaAcceso, DialogoReserva—. Antes, cada aviso mostrado los repintaba a todos; ahora solo se repinta ListaAvisos, que es el único que lee la pila. La ganancia es notable porque los avisos aparecen y desaparecen solos con un temporizador: cada aviso provocaba dos rondas de repintado general.

Solución 3. Son dos causas distintas, y por eso llevan soluciones distintas.

Cabecera con el contador de reservas. Consume ContextoReservas para calcular reservas.filter((r) => r.estado === 'activa').length. Como useContext suscribe al valor entero y el borrador forma parte de ese valor, cada tecla publica un estado nuevo y repinta la Cabecera entera —con su nav, sus NavLink y su MenuUsuario—.

Soluciones, de menor a mayor:

  1. Dividir el estado dentro del reductor: publicar reservas y borrador en contextos distintos, de modo que la Cabecera solo se suscriba a la lista. Es la solución 1 llevada un paso más allá y funciona, pero se nota que estás peleando contra la falta de selección granular.
  2. Extraer el distintivo a su propio componente DistintivoReservas, que consume el contexto, y dejar que Cabecera no lo consuma. Así el repintado queda contenido en un <span> en lugar de en toda la cabecera. Es barato y eficaz.
  3. La solución real es la del apartado 11: esta necesidad —«quiero suscribirme solo a estado.reservas»— es exactamente la señal 1. Con useSelector (07-05) se expresa en una línea y sin dividir nada.

MenuUsuario con el nombre del usuario. Aquí no debería repintarse en absoluto, porque consume ContextoUsuario y la sesión no ha cambiado. Si lo hace, es por una de estas dos causas:

  • El valor de ProveedorUsuario no está memorizado (apartado 5): el proveedor se re-ejecuta por un motivo ajeno y publica un objeto nuevo. Solución: useMemo + useCallback, como en el apartado 7.
  • MenuUsuario también consume ContextoReservas —por ejemplo para mostrar «tienes 2 reservas activas»—. Entonces el repintado no viene de la sesión, y la solución es la misma que la de la Cabecera.

La conclusión que interesa: Cabecera es un problema estructural del contexto que solo se mitiga; MenuUsuario es un fallo evitable que se corrige con memorización. Distinguir cuál de los dos tienes delante es lo que evita refactorizaciones inútiles.

Conclusión

El contexto es una estrategia de gestión de estado completa cuando se usa con el patrón entero: un fichero por dominio, el contexto privado del módulo, el estado en un useState o useReducer dentro del proveedor, un valor expuesto que se decide deliberadamente, el proveedor como componente propio que recibe children y un hook de acceso que lanza si falta el proveedor. ProveedorAvisos es el ejemplo completo de ese patrón, y Proveedores es la respuesta a la pirámide de anidamientos que crece con cada dominio.

Sobre el coste, ahora sabes lo que ocurre de verdad: cualquier consumidor se vuelve a ejecutar cuando cambia el valor del proveedor, aunque solo le interese una parte, porque useContext no ofrece selección granular; y además el valor puede «cambiar» sin cambiar, porque un objeto literal tiene identidad nueva en cada render y React compara con Object.is. Las tres soluciones son complementarias: dividir contextos por frecuencia de cambio y separar estado de acciones —aprovechando que despachar es estable por garantía de React—, estabilizar el valor con useMemo y useCallback, que en un proveedor no es optimización prematura sino obligación, y mover el estado hacia abajo o pasar children para que el subárbol deje de depender del proveedor, que es la única de las tres que reduce complejidad en lugar de añadirla. En CicloUrbano se aplican las tres: tema y usuario memorizados, avisos y reservas divididos en estado y acciones, y todo compuesto en un Proveedores que respeta la colocación de 06-02 y 06-03.

Y sabes dónde está el techo. El contexto no te da herramientas de depuración, ni un punto único donde interceptar cambios, ni viaje en el tiempo, ni selección granular, ni una convención compartida para la lógica asíncrona. Contexto + useReducer es, conceptualmente, Redux: estado centralizado, cambios solo por acciones, reductor puro. Lo que le falta no es el modelo, es la infraestructura que se construye alrededor de ese modelo, empezando por un almacén que vive fuera del árbol de React y por un panel que te enseña qué acción cambió qué. Eso es lo que llega ahora: qué es Redux, qué problema resuelve exactamente, por qué Redux Toolkit es hoy la única forma sensata de usarlo y cómo se monta el almacén de CicloUrbano. La próxima lección es Redux: Introducción y Configuración.

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