El módulo anterior terminó con un diagnóstico incómodo: el estado de CicloUrbano está repartido por toda la aplicación —la sesión en ContextoUsuario, el tema en ContextoTema, los avisos en ContextoAvisos, las reservas en un reductor dentro de ContextoReservas, el filtro del catálogo en la URL y el término de búsqueda en un useState de PaginaCatalogo— y nadie sabría decir con seguridad dónde debe ir el próximo dato que aparezca. La tentación en este punto es elegir una biblioteca y confiar en que ordene el desorden. Es exactamente la decisión equivocada, y en el orden equivocado. Primero se clasifica el estado, después se elige la herramienta, porque la mitad de los problemas que la gente intenta resolver con Redux desaparecen simplemente colocando cada dato donde le corresponde. En esta lección vas a construir una taxonomía del estado, aplicarla al inventario real de CicloUrbano, aprender un árbol de decisión que sirva para cualquier dato nuevo, entender los dos errores simétricos —elevar demasiado y globalizar demasiado pronto—, recuperar la distinción entre estado y valor derivado, y situar en un mapa las opciones disponibles. Al final tendrás una auditoría escrita de CicloUrbano y el plan de lo que reordena cada lección del módulo.

Contenido

  1. Qué significa realmente «gestionar el estado»
  2. La taxonomía: seis tipos de estado
  3. Recorrido tipo por tipo sobre el inventario de CicloUrbano
  4. El árbol de decisión
  5. Los dos errores simétricos: elevación excesiva y globalización prematura
  6. Fuente única de verdad y estado derivado
  7. Panorama de opciones: qué existe y cuándo lo elegirías
  8. Auditoría del estado actual de CicloUrbano
  9. Plan del módulo

  1. Qué significa realmente «gestionar el estado»

Hasta ahora has aprendido mecanismos: useState guarda un valor entre renders, useReducer concentra transiciones, useContext evita pasar props por diez niveles, useSearchParams pone un valor en la URL. Gestionar el estado no es aprender un mecanismo más. Es responder, para cada dato de la aplicación, a tres preguntas:

  1. ¿Quién lo posee? Es decir, cuál es el único sitio donde ese dato existe de verdad. Todo lo demás debe leerlo de ahí, no copiarlo.
  2. ¿Quién lo necesita? El conjunto de componentes que lo leen o lo cambian. Ese conjunto determina hasta dónde tiene que subir.
  3. ¿Cuándo caduca? Un dato local vive lo que vive el componente. Un dato del servidor puede quedarse obsoleto un segundo después de llegar. No son la misma clase de cosa.

Cuando estas tres preguntas se responden mal, aparecen los síntomas conocidos: dos partes de la pantalla que muestran cifras distintas de lo mismo, un formulario que se vacía al navegar, un contador que se reinicia sin motivo, un cambio de tema que repinta el catálogo entero, un useEffect que sincroniza un estado con otro estado. Ninguno de esos síntomas se cura instalando un paquete.

La gestión del estado es, sobre todo, un problema de colocación. La elección de biblioteca es la última decisión, no la primera.

  1. La taxonomía: seis tipos de estado

Esta es la tabla central de la lección. Vale la pena leerla despacio, porque el resto del módulo se apoya en ella.

Tipo Qué es Ejemplo en CicloUrbano Dónde debe vivir Herramienta Si lo pones donde no toca
Local de interfaz Un detalle visual que solo afecta al componente que lo pinta Un desplegable abierto, la pestaña activa de un panel, si MenuUsuario está desplegado Dentro del propio componente useState, useAlternar Elevado: el padre repinta media pantalla al abrir un menú
Compartido entre pocos Un dato que coordinan dos o tres componentes hermanos La bicicleta seleccionada en el catálogo, que resaltan ListaBicicletas y muestra PanelReserva En el ancestro común más cercano Elevar el estado (04-01) + props Global: aparece en un almacén que no le corresponde y nadie sabe quién lo cambia
Global de aplicación Un dato que lee media aplicación y cambia poco Sesión (usuario, esOperario), tema visual, avisos Cerca de la raíz, accesible sin props Contexto, Redux Toolkit, Zustand Pasado por props: prop drilling de seis niveles con props que solo sirven para atravesar
Del servidor Una copia local de datos que viven en una base de datos remota Bicicletas, estaciones, reservas, usuarios En una caché con su propio ciclo de vida TanStack Query, RTK Query, loader de Router En useState o en Redux: te toca escribir a mano carga, error, cancelación, revalidación e invalidación
De la URL Un dato que debe poder compartirse por enlace o sobrevivir a un recargado El filtro del catálogo (?tipo=electrica), el bicicletaId de la ficha En la propia dirección useSearchParams, useParams (06-02) En useState: el enlace que envías a un compañero no muestra lo que tú ves
De formulario Los valores que el usuario está escribiendo, más su validación El borrador de FormularioReserva Junto al formulario, o en un reductor propio useState controlado, useReducer, React Hook Form Global: cada tecla despacha una acción y repinta todo lo que lea el almacén

Fíjate en un detalle que va a resultar decisivo en 07-06: el estado del servidor no es una variante del estado global, es una categoría aparte. Comparte con él que lo lee media aplicación, pero se diferencia en lo esencial: no lo posees tú, se queda obsoleto solo y otro puede cambiarlo mientras tú lo miras.

  1. Recorrido tipo por tipo sobre el inventario de CicloUrbano

La tabla es abstracta hasta que se aplica. Vamos dato por dato con lo que hay escrito hoy en el proyecto.

3.1 Local de interfaz: el desplegable de MenuUsuario

// src/componentes/MenuUsuario.jsx
function MenuUsuario() {
  const [abierto, alternar] = useAlternar(false);   // hook de 05-06
  const { usuario, cerrarSesion } = useUsuario();
  // …
}

abierto es el ejemplo canónico de estado local: nadie fuera de MenuUsuario necesita saberlo, no tiene que sobrevivir a una navegación y no tiene sentido compartirlo por enlace. Si lo elevaras a Diseno, abrir el menú provocaría un render de Diseno y, con él, de Cabecera, MigasDePan y todo el <Outlet />. Coste enorme a cambio de nada.

Regla: si al escribir el estado en el componente nadie protesta, déjalo ahí. El estado sube cuando alguien lo necesita, no por si acaso.

3.2 Compartido entre pocos: la bicicleta seleccionada

En el catálogo, ListaBicicletas marca visualmente la tarjeta elegida y PanelReserva muestra sus datos. Son hermanos, así que el dato vive en su ancestro común, PaginaCatalogo, y baja por props. Es exactamente lo que hiciste en 04-01.

// src/paginas/PaginaCatalogo.jsx (fragmento)
const [idSeleccionada, setIdSeleccionada] = useState(null);
// …
<ListaBicicletas bicicletas={visibles} idSeleccionada={idSeleccionada} alSeleccionar={setIdSeleccionada} />
<PanelReserva bicicleta={visibles.find((bici) => bici.id === idSeleccionada)} />

Dos niveles de props no son un problema. El prop drilling empieza a doler a partir de tres o cuatro niveles de componentes que reciben una prop solo para pasarla, sin usarla.

3.3 Global de aplicación: sesión, tema y avisos

Los tres cumplen el mismo perfil: los lee mucha gente, están lejos entre sí en el árbol y cambian poco.

Dato Quién lo lee Frecuencia de cambio
usuario, esOperario Cabecera, MenuUsuario, RutaProtegida, RequiereRol, PaginaTaller, FormularioReserva Muy baja: al entrar y al salir
Tema visual BotonTema y el atributo data-tema del documento Muy baja: cuando el usuario lo pulsa
Avisos ListaAvisos los pinta; cualquiera puede lanzarlos Media: en cada operación que informa

Que la frecuencia de cambio sea baja es lo que hace del contexto una elección razonable para los dos primeros. Con los avisos ya empieza a haber matices, y ese matiz es el tema de 07-02.

3.4 Del servidor: bicicletas, estaciones, reservas y usuarios

Hoy, en CicloUrbano, viven en src/datos/dominio.js como constantes importadas. Eso es una simplificación didáctica que ha aguantado seis módulos, pero es mentira: en una aplicación real, bici-002 está alquilada porque alguien la alquiló hace tres minutos desde otro dispositivo, y tu copia local no se ha enterado.

// src/datos/dominio.js — el atajo que hemos usado hasta ahora
export const bicicletas = [
  { id: 'bici-001', modelo: 'Urbana Clásica', tipo: 'urbana', estado: 'disponible', estacionId: 'est-01', precioHora: 2.5 },
  { id: 'bici-002', modelo: 'Eléctrica Pro', tipo: 'electrica', estado: 'alquilada', estacionId: 'est-01', precioHora: 4.0 },
  // …
];

En 07-06 esto pasa a una API ficticia servida con json-server, y con ella aparece toda la lista de problemas que justifica una biblioteca dedicada. De momento basta con marcarlo en el inventario: estos cuatro conjuntos no son estado del cliente.

3.5 De la URL: el filtro del catálogo

Ya lo resolviste en 06-02: ?tipo=electrica vive en la dirección, se lee con useSearchParams y tipoElegido no existe como useState. La prueba de que la decisión fue correcta es que puedes copiar la barra de direcciones, pegársela a un compañero y ver los dos lo mismo. Ese es el criterio: si el dato debe poder viajar en un enlace, va en la URL, y en ningún otro sitio.

Un aviso importante para el resto del módulo: cuando llegue Redux, habrá una tentación clarísima de mover el filtro al almacén «para tenerlo todo junto». No lo hagas. Duplicaría la fuente de verdad y tendrías que sincronizar URL y almacén con un efecto, que es justo lo que evita useSearchParams.

3.6 De formulario: el borrador de la reserva

El borrador de FormularioReserva es efímero, cambia en cada pulsación y solo interesa mientras el formulario está abierto. Vive en reductorReservas porque comparte transiciones con el envío (envio_iniciado, reserva_creada, envio_fallido), no porque sea global. Es un caso de libro de por qué la clasificación importa: el borrador está en un contexto, pero no es estado global.

  1. El árbol de decisión

Cada vez que aparezca un dato nuevo, recórrelo de arriba abajo y para en la primera respuesta afirmativa.

flowchart TD
    A["Dato nuevo"] --> B{"¿Viene de un servidor?"}
    B -- Sí --> C["Caché de estado de servidor<br/>TanStack Query · 07-06"]
    B -- No --> D{"¿Debe poder compartirse<br/>por enlace o sobrevivir<br/>a un recargado?"}
    D -- Sí --> E["URL<br/>useSearchParams / useParams"]
    D -- No --> F{"¿Lo usa un solo<br/>componente?"}
    F -- Sí --> G["useState local"]
    F -- No --> H{"¿Unos pocos hermanos<br/>cercanos?"}
    H -- Sí --> I["Elevar al ancestro común<br/>+ props"]
    H -- No --> J{"¿Lo lee media aplicación<br/>y cambia poco?"}
    J -- Sí --> K["Contexto · 07-02"]
    J -- No --> L{"¿Cambia a menudo,<br/>hay muchos consumidores<br/>o la lógica es compleja?"}
    L -- Sí --> M["Almacén dedicado<br/>Redux Toolkit · 07-03…07-05"]
    L -- No --> K

Dos observaciones sobre el árbol:

  • La primera pregunta es la del servidor, y es deliberado. Es la que más gente se salta y la que más trabajo ahorra acertar. Meter datos remotos en un almacén de cliente es el error de arquitectura más caro de este módulo.
  • La rama de Redux es la última. No porque Redux sea malo, sino porque llegar a ella significa haber descartado antes cinco alternativas más baratas. Cuando llegas a Redux habiendo descartado esas cinco, Redux es una excelente decisión.

  1. Los dos errores simétricos: elevación excesiva y globalización prematura

Los principiantes ponen el estado demasiado abajo y sufren; los intermedios sobrecorrigen y lo ponen demasiado arriba. Ambos extremos tienen síntomas reconocibles.

5.1 Elevación excesiva

Consiste en subir un estado más arriba de donde hace falta «por si algún día lo necesita alguien más».

Síntomas concretos:

  • Abrir un desplegable repinta la cabecera, las migas de pan y la página entera.
  • Componentes que reciben cinco props de las que usan una: las otras cuatro solo pasan de largo.
  • Un padre con nueve useState que no tienen nada que ver entre sí.
  • Cambiar un componente hoja obliga a tocar tres ficheros por encima.
// ANTIPATRÓN: el estado de un detalle visual, elevado sin necesidad
function Diseno() {
  const [menuAbierto, setMenuAbierto] = useState(false);   // ← solo lo usa MenuUsuario
  return (
    <div>
      <Cabecera menuAbierto={menuAbierto} alAlternarMenu={setMenuAbierto} />
      <Outlet />
    </div>
  );
}

Cabecera no usa menuAbierto: solo lo transporta hasta MenuUsuario. Y cada pulsación del menú provoca un render de Diseno, es decir, de toda la aplicación.

5.2 Globalización prematura

Consiste en instalar una biblioteca de estado global antes de tener un problema que la justifique.

Síntomas concretos:

  • Un almacén con menuAbierto, modalVisible y pestanaActiva: estado local disfrazado de global.
  • Acciones que solo despacha un componente y que solo lee ese mismo componente.
  • Cinco ficheros tocados (acción, reductor, selector, componente, prueba) para añadir una casilla de verificación.
  • Nadie puede eliminar un campo del almacén porque nadie sabe quién lo lee.
Elevación excesiva Globalización prematura
Qué se hace mal El estado sube más de lo necesario Todo acaba en un almacén único
Coste principal Renders innecesarios y props de paso Complejidad, indirección y ceremonia
Cómo se detecta Props que no se usan, renders en cascada Estado local dentro del almacén
Cómo se corrige Bajar el estado al componente que lo usa Sacar del almacén lo que no sea global

La corrección de ambos es la misma frase: el estado debe vivir en el punto más bajo posible que siga siendo común a todos sus consumidores. Ni más arriba, ni más abajo.

  1. Fuente única de verdad y estado derivado

Recuperamos aquí, con más consecuencias, algo que ya viste en 05-01: no guardes como estado nada que puedas calcular a partir de otro estado.

// ANTIPATRÓN: estado duplicado y sincronizado con un efecto
const [bicicletas, setBicicletas] = useState(datosIniciales);
const [tipo, setTipo] = useState('todos');
const [visibles, setVisibles] = useState(datosIniciales);   // ← derivado guardado como estado

useEffect(() => {
  setVisibles(bicicletas.filter((bici) => tipo === 'todos' || bici.tipo === tipo));
}, [bicicletas, tipo]);

Tres problemas encadenados: hay un render de más (React pinta con visibles desactualizado y vuelve a pintar tras el efecto), existe una ventana en la que visibles y tipo se contradicen, y cualquiera que en el futuro cambie bicicletas sin pasar por el efecto rompe la coherencia.

// CORRECTO: el derivado se calcula durante el render
const [bicicletas, setBicicletas] = useState(datosIniciales);
const tipo = parametros.get('tipo') ?? 'todos';           // la URL es la fuente de verdad

const visibles = bicicletas.filter((bici) => tipo === 'todos' || bici.tipo === tipo);

visibles deja de existir como estado. No puede desincronizarse, porque se recalcula en cada render a partir de las dos únicas fuentes de verdad. Si algún día ese cálculo resulta caro, la respuesta es memorizarlo con useMemo (08-03), no convertirlo en estado.

Casos de estado derivado que aparecen en CicloUrbano y que no deben guardarse:

Valor Se calcula a partir de Nunca lo guardes porque
esOperario usuario.rol === 'operario' Se desincroniza al cambiar de sesión
visibles bicicletas + filtro de la URL + término de búsqueda Se desincroniza en cada cambio de filtro
puedeEnviar Errores de validación + estadoEnvio Se desincroniza al escribir en el formulario
totalReserva precioHora × horas Se desincroniza al cambiar las horas
plazasLibres estacion.plazas − bicicletas en la estación Se desincroniza al mover una bicicleta

Regla: si puedes escribir el valor como una expresión de otros valores, es un derivado. Escríbelo como expresión.

  1. Panorama de opciones: qué existe y cuándo lo elegirías

Esta tabla es el mapa del terreno. No es una recomendación única: la columna que importa es «cuándo lo elegirías».

Opción Qué resuelve Cuándo lo elegirías Coste de entrada
Props + elevar el estado Coordinar unos pocos componentes cercanos Siempre que la distancia sea de uno a tres niveles Nulo: ya está en React
Contexto Evitar props de paso para datos ambientales Sesión, tema, idioma, avisos: pocos cambios, muchos lectores Bajo, pero con coste de rendimiento si el valor cambia a menudo (07-02)
useReducer + contexto Lógica de transición compleja compartida Un dominio con muchas acciones y un árbol amplio de consumidores Bajo: no hay dependencias nuevas
Redux Toolkit Almacén único, acciones trazables, herramientas de depuración Aplicaciones grandes, equipos de varias personas, lógica de estado que hay que auditar Medio: una dependencia, un vocabulario y una estructura de carpetas
Zustand Almacén global mínimo con selección granular, sin proveedor Quieres estado global sin la ceremonia de Redux y no necesitas viaje en el tiempo Muy bajo: un fichero y un hook
Jotai Estado atómico: unidades pequeñas que se combinan y solo repintan a quien las lee Muchas piezas independientes de estado con dependencias entre ellas Bajo, pero exige pensar en átomos, no en objetos
TanStack Query Estado del servidor: caché, carga, error, revalidación, invalidación En cuanto la aplicación lea datos de una API. Casi siempre Medio, y se amortiza en la primera pantalla

Sobre Zustand y Jotai, lo justo para situarlos y sin tutorial, porque este módulo enseña Redux Toolkit:

  • Zustand define un almacén como una función que devuelve estado y acciones, y los componentes se suscriben con un selector: const usuario = useAlmacen((s) => s.usuario). No hay proveedor, no hay acciones con type y solo se repinta quien lee lo que cambió. Es la alternativa habitual cuando Redux parece demasiada ceremonia.
  • Jotai invierte el modelo: en lugar de un objeto grande, define átomos pequeños (const atomoTema = atom('claro')) que se leen con useAtom. Los átomos derivados se recalculan solos y solo repinta quien depende del átomo que cambió.

Ambas son opciones legítimas y ninguna sustituye a TanStack Query para los datos remotos. La elección más común hoy en una aplicación mediana no es «Redux o contexto», sino «una herramienta para el estado del cliente y otra para el del servidor».

  1. Auditoría del estado actual de CicloUrbano

Con la taxonomía en la mano, esta es la foto de la aplicación al final del módulo 6 y el veredicto de cada pieza.

Dato Dónde está hoy Tipo real Veredicto Lección que lo trata
usuario, esOperario, cargandoSesion ContextoUsuario Global Correcto; el valor debe estabilizarse 07-02, luego sliceSesion en 07-04
Tema visual ContextoTema Global Correcto y se queda así todo el módulo 07-02
avisos, mostrarAviso ContextoAvisos Global Correcto, pero hay que separar estado de acciones 07-02
reservas, borrador, estadoEnvio, error ContextoReservas con useReducer Mezcla: la lista es del servidor, el borrador es de formulario A dividir 07-04 y 07-06
Filtro ?tipo= URL De URL Correcto; no se mueve a Redux Se mantiene
termino de búsqueda useState en PaginaCatalogo Local hoy, compartido en cuanto lo lea otra pantalla Revisable 07-04 (sliceCatalogo)
bicicletas, estaciones Constantes en src/datos/dominio.js Del servidor A mover a una caché de consultas 07-06
idSeleccionada useState en PaginaCatalogo Compartido entre pocos Correcto: se queda donde está Se mantiene
abierto de MenuUsuario, modales useState / useAlternar locales Local de interfaz Correcto: nunca entra en el almacén Se mantiene

Dos conclusiones de la auditoría que conviene subrayar antes de seguir:

  1. La mayor parte de lo que hay está bien colocado. El módulo no va a tirar el trabajo de los módulos 5 y 6: va a reordenar tres cosas y a extraer una cuarta.
  2. El problema más grave no es el contexto, es que los datos de dominio se tratan como estado del cliente. Es lo que arregla 07-06, y es el cambio con más impacto de todo el módulo.

  1. Plan del módulo

flowchart LR
    A["07-01<br/>Clasificar"] --> B["07-02<br/>Contexto bien hecho"]
    B --> C["07-03<br/>Almacén Redux"]
    C --> D["07-04<br/>Slices y selectores"]
    D --> E["07-05<br/>Conectar a React"]
    E --> F["07-06<br/>Estado del servidor"]
Lección Qué reordena de CicloUrbano
07-02 API de Contexto Divide ContextoReservas en estado y acciones, compone los cuatro proveedores en un solo Proveedores y establece dónde deja de rendir el contexto
07-03 Redux: Introducción Crea src/almacen/almacen.js con configureStore y lo provee en main.jsx, con DevTools funcionando
07-04 Acciones y Reductores Escribe sliceReservas, sliceCatalogo y sliceSesion con sus selectores y su carga asíncrona
07-05 Conectando a React Reescribe PaginaCatalogo, PaginaReservas, PanelReservas y MenuUsuario con useSelector y useDispatch, y decide qué se queda fuera
07-06 Estado del Servidor Levanta la API con json-server, sustituye useFetchBicicletas por useBicicletas sobre TanStack Query y fija la arquitectura final

Errores Comunes y Consejos

Error 1: elegir la biblioteca antes de clasificar el estado. «Vamos a usar Redux» no es una decisión de arquitectura, es un aplazamiento. Clasifica primero; a menudo descubrirás que la mitad de tu «estado global» era estado del servidor y la otra mitad, estado local.

Error 2: tratar los datos del servidor como estado normal. Es el error más caro y el más frecuente. Si el dato tiene un id que existe en una base de datos, no es tuyo: es una copia que caduca.

Error 3: guardar derivados. Cada useEffect cuya única misión es hacer setAlgo(...) a partir de otro estado es un derivado mal guardado. Bórralo y calcula la expresión durante el render.

Error 4: mover a un almacén global lo que ya vive en la URL. Acabas con dos fuentes de verdad y un efecto que las sincroniza. La URL gana siempre para lo que debe ser compartible por enlace.

Error 5: confundir «lo usan muchos componentes» con «cambia mucho». Son ejes distintos y determinan cosas distintas: el primero decide dónde vive, el segundo decide con qué herramienta. Un dato leído por veinte componentes que cambia una vez por sesión es el caso ideal del contexto; uno leído por veinte componentes que cambia en cada pulsación es el caso peor.

Consejo 1: escribe el inventario. Una tabla de tres columnas —dato, tipo, dónde vive— en el README del proyecto evita meses de discusiones. Cuando alguien añada un dato nuevo, la tabla le dirá dónde ponerlo.

Consejo 2: empieza siempre por el useState local. Subir un estado después es un refactor de diez minutos. Bajarlo desde un almacén global al que ya se han enganchado ocho componentes es un refactor de una tarde.

Consejo 3: mide antes de optimizar la colocación. Si sospechas que un contexto está provocando renders de más, compruébalo con el Profiler (08-05) en lugar de reestructurar a ciegas.

Ejercicios

Ejercicio 1. Clasifica estos seis datos nuevos de CicloUrbano según la taxonomía del apartado 2 e indica dónde deben vivir y con qué herramienta:

  1. El texto que el operario escribe en el buscador de la PaginaTaller.
  2. La lista de incidencias de una estación, que llega de GET /estaciones/est-02/incidencias.
  3. La unidad de precio elegida (€/hora o €/día), que afecta a todas las tarjetas y a la ficha.
  4. Si la tarjeta TarjetaBicicleta muestra su descripción larga desplegada.
  5. El orden del catálogo (por precio o por modelo), que debe poder enviarse por enlace.
  6. El número de reservas activas que muestra el distintivo de la Cabecera.

Ejercicio 2. El siguiente componente tiene tres problemas de gestión del estado. Identifícalos y reescríbelo.

function PaginaEstaciones() {
  const [estaciones, setEstaciones] = useState([]);
  const [barrio, setBarrio] = useState('todos');
  const [visibles, setVisibles] = useState([]);
  const [totalPlazas, setTotalPlazas] = useState(0);
  const [detalleAbierto, setDetalleAbierto] = useState(null);

  useEffect(() => {
    setVisibles(estaciones.filter((est) => barrio === 'todos' || est.barrio === barrio));
  }, [estaciones, barrio]);

  useEffect(() => {
    setTotalPlazas(visibles.reduce((suma, est) => suma + est.plazas, 0));
  }, [visibles]);

  // …
}

Ejercicio 3. Un compañero propone: «Vamos a poner en Redux toda la aplicación: las bicicletas, el filtro del catálogo, el tema, el menú desplegable de MenuUsuario y el borrador del formulario de reserva. Así está todo en un sitio y siempre sabemos dónde mirar.» Escribe una respuesta razonada, dato por dato, indicando cuáles sí y cuáles no, y por qué.

Soluciones

Solución 1.

Dato Tipo Dónde vive Herramienta Razonamiento
1. Buscador del taller Local de interfaz (o de formulario) En PaginaTaller useState + useDebounce Solo lo usa esa pantalla y no tiene sentido compartirlo por enlace. Si el equipo decide que sí debe ser enlazable, pasa a la URL
2. Incidencias de una estación Del servidor Caché de consultas useQuery(['estaciones', estacionId, 'incidencias']) Viene de una API: la primera pregunta del árbol se responde afirmativamente y ahí termina el recorrido
3. Unidad de precio Global de aplicación Cerca de la raíz Contexto (o sliceCatalogo si ya usas Redux) Muchos lectores, cambios muy poco frecuentes: perfil idéntico al del tema
4. Descripción desplegada Local de interfaz Dentro de TarjetaBicicleta useAlternar Un detalle visual por tarjeta. Elevarlo obligaría a guardar un mapa de identificadores abiertos para nada
5. Orden del catálogo De la URL En la dirección useSearchParams, junto a ?tipo= El enunciado lo dice: debe poder enviarse por enlace
6. Número de reservas activas Derivado del estado del servidor No se guarda reservas.filter((r) => r.estado === 'activa').length No es estado: es una cuenta sobre la lista de reservas. Guardarlo garantiza que algún día muestre una cifra distinta de la lista

El caso 6 es el más instructivo: la respuesta correcta no es «dónde lo guardo» sino «no lo guardes».

Solución 2. Los tres problemas:

  1. visibles es un derivado guardado como estado, sincronizado con un efecto. Sobra el estado y sobra el efecto.
  2. totalPlazas es un derivado de un derivado, con un segundo efecto encadenado. Cada cambio de barrio provoca tres renders: uno con datos viejos, otro tras el primer efecto y otro tras el segundo.
  3. estaciones es estado del servidor guardado en useState, sin carga, sin error y sin cancelación. Es el candidato de 07-06.
function PaginaEstaciones() {
  // 3) Estado del servidor: en 07-06 esto pasa a useQuery
  const { data: estaciones = [], isPending, isError } = useEstaciones();

  // Estado real: solo dos piezas
  const [barrio, setBarrio] = useState('todos');
  const [detalleAbierto, setDetalleAbierto] = useState(null);

  // 1) y 2) Derivados: expresiones, no estado
  const visibles = estaciones.filter((est) => barrio === 'todos' || est.barrio === barrio);
  const totalPlazas = visibles.reduce((suma, est) => suma + est.plazas, 0);

  if (isPending) return <IndicadorDeCarga mensaje="Cargando estaciones…" />;
  if (isError) return <Aviso tono="error" texto="No se han podido cargar las estaciones." />;
  // …
}

De cinco variables de estado se pasa a dos. Los dos useEffect desaparecen, y con ellos los renders intermedios y las ventanas de incoherencia. detalleAbierto se queda: es estado local de interfaz legítimo.

Solución 3. Respuesta razonada, dato por dato:

  • Bicicletas: no. Son estado del servidor. En Redux tendrías que escribir a mano la carga, el error, la cancelación, la revalidación al volver a la pestaña y la invalidación tras cada escritura. Van a una caché de consultas (07-06). Si el equipo prefiere no añadir otra dependencia, la alternativa razonable es RTK Query, que es la misma idea dentro de Redux, no un slice a mano.
  • Filtro del catálogo: no. Ya vive en la URL, que es la fuente de verdad correcta porque debe poder compartirse por enlace y sobrevivir a un recargado. Moverlo al almacén crearía una segunda copia y un efecto de sincronización, con la incoherencia garantizada.
  • Tema: no hace falta. Cambia una vez por sesión y lo lee un BotonTema más el atributo data-tema del documento. El contexto lo resuelve sin dependencias. Meterlo en Redux no está mal, pero no aporta nada: no hay lógica que auditar ni acciones que trazar.
  • Menú desplegable de MenuUsuario: rotundamente no. Es estado local de interfaz. En el almacén lo convertirías en global, cada apertura despacharía una acción que ensuciaría el historial de DevTools y obligaría a comprobar quién más lee ese campo antes de tocarlo.
  • Borrador del formulario: no. Cambia en cada pulsación. En el almacén, cada tecla sería una acción y un ciclo de comparación para todos los suscriptores. Se queda junto al formulario, en useState o en su reductor.
  • Lo que sí iría a Redux: la sesión (sliceSesion), los criterios persistentes del catálogo que no van en la URL —como el término de búsqueda si acaba compartiéndose entre pantallas— y la lógica de reservas del cliente, que es la que tiene transiciones que merece la pena auditar.

Y el argumento de fondo contra la propuesta: «tenerlo todo en un sitio» no es una ventaja si ese sitio deja de decirte nada. Un almacén con doscientos campos, de los cuales ciento cincuenta son locales, es tan difícil de razonar como no tener almacén; además hace que la herramienta más valiosa de Redux —el historial de acciones— quede sepultada bajo el ruido de aperturas de menú.

Conclusión

Antes de instalar nada, has puesto orden. Sabes que gestionar el estado es responder a tres preguntas por cada dato —quién lo posee, quién lo necesita y cuándo caduca— y que la respuesta se organiza en seis tipos: local de interfaz, compartido entre pocos, global de aplicación, del servidor, de la URL y de formulario, cada uno con su sitio natural y su herramienta. Tienes un árbol de decisión que empieza por la pregunta que más ahorra —«¿viene de un servidor?»— y que deja el almacén dedicado como última rama, la que se alcanza después de descartar cinco alternativas más baratas. Conoces los dos errores simétricos y sus síntomas: la elevación excesiva, que convierte cada pulsación en un render de toda la aplicación y llena los componentes de props de paso, y la globalización prematura, que mete menuAbierto en un almacén y obliga a tocar cinco ficheros para añadir una casilla. Y has recuperado con más peso la regla de 05-01: una única fuente de verdad y todo lo demás calculado, porque visibles, esOperario, puedeEnviar, totalReserva y plazasLibres no son estado y guardarlos solo garantiza que algún día se contradigan.

Sobre ese armazón has situado las opciones reales —props y elevar, contexto, useReducer + contexto, Redux Toolkit, Zustand, Jotai y TanStack Query— con su coste de entrada y su momento adecuado, y has hecho la auditoría de CicloUrbano: la mayor parte está bien colocada, el filtro seguirá en la URL y los desplegables seguirán siendo locales, pero ContextoReservas mezcla estado de formulario con datos que en realidad son del servidor, y src/datos/dominio.js está fingiendo ser una base de datos.

La primera pieza a reordenar es la que ya tienes en las manos. El contexto no es solo un mecanismo para evitar props: es una estrategia de gestión de estado, con un patrón completo de módulo por dominio, un coste de rendimiento muy concreto que hasta ahora solo se había mencionado de pasada, y un límite claro más allá del cual conviene otra herramienta. La próxima lección es API de Contexto.

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