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
- Qué significa realmente «gestionar el estado»
- La taxonomía: seis tipos de estado
- Recorrido tipo por tipo sobre el inventario de CicloUrbano
- El árbol de decisión
- Los dos errores simétricos: elevación excesiva y globalización prematura
- Fuente única de verdad y estado derivado
- Panorama de opciones: qué existe y cuándo lo elegirías
- Auditoría del estado actual de CicloUrbano
- Plan del módulo
- 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:
- ¿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.
- ¿Quién lo necesita? El conjunto de componentes que lo leen o lo cambian. Ese conjunto determina hasta dónde tiene que subir.
- ¿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.
- 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.
- 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.
- 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.
- 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
useStateque 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,modalVisibleypestanaActiva: 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.
- 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.
- 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 contypey 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 conuseAtom. 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».
- 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:
- 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.
- 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.
- 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:
- El texto que el operario escribe en el buscador de la
PaginaTaller. - La lista de incidencias de una estación, que llega de
GET /estaciones/est-02/incidencias. - La unidad de precio elegida (€/hora o €/día), que afecta a todas las tarjetas y a la ficha.
- Si la tarjeta
TarjetaBicicletamuestra su descripción larga desplegada. - El orden del catálogo (por precio o por modelo), que debe poder enviarse por enlace.
- 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:
visibleses un derivado guardado como estado, sincronizado con un efecto. Sobra el estado y sobra el efecto.totalPlazases un derivado de un derivado, con un segundo efecto encadenado. Cada cambio debarrioprovoca tres renders: uno con datos viejos, otro tras el primer efecto y otro tras el segundo.estacioneses estado del servidor guardado enuseState, 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
slicea 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
BotonTemamás el atributodata-temadel 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
useStateo 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
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
