En la lección anterior quedó claro qué es el enrutamiento en el cliente, por qué React no lo trae de serie y cuál de las tres formas de usar React Router v7 emplea este curso: el modo de datos, con createBrowserRouter y RouterProvider. Ahora toca escribirlo. En esta lección conviertes CicloUrbano de una aplicación de una sola pantalla en una aplicación enrutada: instalarás el paquete, crearás el mapa de rutas en src/rutas.jsx, montarás el proveedor del enrutador en main.jsx sin romper el orden de proveedores que ya existe, reorganizarás el proyecto con una carpeta src/paginas/, sustituirás los <a href="#…"> de la Cabecera por enlaces de verdad —saldando por fin la deuda del módulo 2—, leerás el identificador de la ficha con useParams y trasladarás el filtro del SelectorTipo desde useState hasta la propia URL con useSearchParams. Al terminar, cada pantalla de CicloUrbano tendrá su dirección.
Contenido
- Punto de partida y plan de la reorganización
- Instalación del paquete
- La carpeta
src/paginas/y qué se queda ensrc/componentes/ - El mapa de rutas:
src/rutas.jsx - Montar el enrutador en
main.jsx - Por qué los proveedores van fuera del enrutador
- Enlaces:
<Link>frente a<a href> <NavLink>y la clase activa: la nav deCabecera- Segmentos dinámicos y
useParams - Qué hacer cuando el identificador no existe
- Parámetros de consulta con
useSearchParams - La ruta comodín y la página de no encontrado
- Cómo elige React Router la mejor coincidencia
- Punto de partida y plan de la reorganización
Así está hoy App.jsx, al final del módulo 5: una única pantalla que muestra el catálogo con su buscador, su filtro y su formulario de reserva, todo envuelto en Diseno.
// src/App.jsx — el punto de partida (versión resumida)
import Diseno from './componentes/Diseno.jsx';
import BuscadorBicicletas from './componentes/BuscadorBicicletas.jsx';
import SelectorTipo from './componentes/SelectorTipo.jsx';
import ListaBicicletas from './componentes/ListaBicicletas.jsx';
import PanelReserva from './componentes/PanelReserva.jsx';
import { useState } from 'react';
import { bicicletas, estaciones } from './datos/dominio.js';
function App() {
const [termino, setTermino] = useState('');
const [tipoElegido, setTipoElegido] = useState('todos');
const [idSeleccionada, setIdSeleccionada] = useState(null);
const visibles = bicicletas
.filter((bici) => tipoElegido === 'todos' || bici.tipo === tipoElegido)
.filter((bici) => bici.modelo.toLowerCase().includes(termino.toLowerCase()));
return (
<Diseno>
<BuscadorBicicletas termino={termino} alCambiarTermino={setTermino} />
<SelectorTipo tipoElegido={tipoElegido} alCambiarTipo={setTipoElegido} />
<ListaBicicletas
bicicletas={visibles}
estaciones={estaciones}
idSeleccionada={idSeleccionada}
alSeleccionar={setIdSeleccionada}
/>
<PanelReserva />
</Diseno>
);
}
export default App;El plan de esta lección, en orden:
flowchart LR
A["1. npm install<br/>react-router"] --> B["2. Crear src/paginas/<br/>con una pantalla por ruta"]
B --> C["3. src/rutas.jsx<br/>createBrowserRouter"]
C --> D["4. main.jsx<br/>RouterProvider"]
D --> E["5. Cabecera<br/>NavLink"]
E --> F["6. useParams<br/>useSearchParams"]
Al terminar, App.jsx desaparece como pantalla: su contenido pasa a PaginaCatalogo y el papel de raíz lo asume el enrutador. Es la reestructuración más grande del curso hasta ahora, así que conviene hacerla por pasos y comprobar que la aplicación arranca en cada uno.
- Instalación del paquete
Eso es todo. Ni complementos de Vite, ni configuración adicional: en el modo de datos, React Router es una dependencia normal.
Sobre el nombre del paquete, porque es la primera confusión con la que te vas a encontrar:
| Paquete | Versión | Qué es |
|---|---|---|
react-router-dom |
v6 y anteriores | El paquete que se instalaba en aplicaciones web. Exportaba BrowserRouter, Link, etc. |
react-router |
v6 y anteriores | El núcleo independiente de plataforma. No se instalaba directamente |
react-router |
v7 | El único paquete que necesitas. De aquí sale todo |
react-router-dom |
v7 | Sigue publicándose, pero solo reexporta react-router. Existe para no romper proyectos migrados |
Consecuencia práctica: todos los ejemplos de internet que veas con from 'react-router-dom' funcionan igual cambiando el importe a from 'react-router'. Elige uno y sé coherente; tener los dos instalados es una fuente de errores de contexto muy difíciles de diagnosticar, porque puedes acabar con dos copias del enrutador en memoria.
Comprueba la versión instalada:
npm list react-router
# [email protected]
# └── [email protected]
- La carpeta
src/paginas/ y qué se queda en src/componentes/
src/paginas/ y qué se queda en src/componentes/Antes de escribir el mapa hay que decidir dónde vive cada cosa. Introducimos una carpeta nueva:
src/ ├── componentes/ ← piezas reutilizables de interfaz ├── contextos/ ├── datos/ ├── hooks/ ├── paginas/ ← NUEVO: una pantalla por ruta ├── reductores/ ├── utilidades/ ├── main.jsx └── rutas.jsx ← NUEVO: el mapa de rutas
La regla que separa ambas carpetas:
src/paginas/ |
src/componentes/ |
|
|---|---|---|
| Qué es | La pantalla completa asociada a una ruta | Una pieza de interfaz reutilizable |
| Quién lo renderiza | El enrutador, y solo él | Otros componentes o páginas |
| Cuántas veces aparece | Una, en su ruta | Las que haga falta |
Puede leer useParams |
Sí, es su sitio natural | Preferiblemente no: recíbelo por props |
| Ejemplos | PaginaCatalogo, PaginaAcceso |
TarjetaBicicleta, Aviso, Modal |
Ese último punto es más que una convención de orden: una página puede depender del enrutador; un componente reutilizable, cuanto menos, mejor. Si TarjetaBicicleta llama a useParams() por dentro, deja de poder usarse en una pantalla que no tenga ese parámetro, y probarla en el módulo 9 obligará a envolverla en un enrutador falso. La página lee el parámetro y se lo pasa por props: el componente sigue siendo una función de sus props, como en 02-03.
Estas son las páginas de CicloUrbano. Empezamos con versiones mínimas y las iremos completando:
// src/paginas/PaginaCatalogo.jsx
import { useState } from 'react';
import BuscadorBicicletas from '../componentes/BuscadorBicicletas.jsx';
import SelectorTipo from '../componentes/SelectorTipo.jsx';
import ListaBicicletas from '../componentes/ListaBicicletas.jsx';
import { bicicletas, estaciones } from '../datos/dominio.js';
function PaginaCatalogo() {
const [termino, setTermino] = useState('');
const [tipoElegido, setTipoElegido] = useState('todos');
const visibles = bicicletas
.filter((bici) => tipoElegido === 'todos' || bici.tipo === tipoElegido)
.filter((bici) => bici.modelo.toLowerCase().includes(termino.toLowerCase()));
return (
<section>
<h2>Catálogo de bicicletas</h2>
<BuscadorBicicletas termino={termino} alCambiarTermino={setTermino} />
<SelectorTipo tipoElegido={tipoElegido} alCambiarTipo={setTipoElegido} />
<ListaBicicletas bicicletas={visibles} estaciones={estaciones} />
</section>
);
}
export default PaginaCatalogo;// src/paginas/PaginaEstaciones.jsx
import TarjetaEstacion from '../componentes/TarjetaEstacion.jsx';
import { estaciones, bicicletas } from '../datos/dominio.js';
function PaginaEstaciones() {
return (
<section>
<h2>Estaciones</h2>
<ul>
{estaciones.map((estacion) => (
<li key={estacion.id}>
<TarjetaEstacion
estacion={estacion}
bicicletasEnEstacion={bicicletas.filter((b) => b.estacionId === estacion.id)}
/>
</li>
))}
</ul>
</section>
);
}
export default PaginaEstaciones;// src/paginas/PaginaReservas.jsx
import { useReservas } from '../contextos/ContextoReservas.jsx';
import PanelReservas from '../componentes/PanelReservas.jsx';
function PaginaReservas() {
const { estado } = useReservas();
return (
<section>
<h2>Mis reservas</h2>
<PanelReservas reservas={estado.reservas} />
</section>
);
}
export default PaginaReservas;PaginaAcceso la desarrollarás en 06-05 (necesita iniciarSesion y la vuelta al destino original), así que de momento basta con un esqueleto. Y falta la más importante para esta lección:
// src/paginas/PaginaNoEncontrada.jsx
import { Link } from 'react-router';
function PaginaNoEncontrada() {
return (
<section>
<h2>Esta página no existe</h2>
<p>
La dirección que has escrito no corresponde a ninguna pantalla de CicloUrbano.
Puede que el enlace esté anticuado o que haya un error tipográfico.
</p>
<Link to="/">Volver al catálogo</Link>
</section>
);
}
export default PaginaNoEncontrada;
- El mapa de rutas:
src/rutas.jsx
src/rutas.jsxAquí está el corazón del modo de datos: el mapa es un array de objetos JavaScript, no marcado JSX.
// src/rutas.jsx
import { createBrowserRouter } from 'react-router';
import PaginaCatalogo from './paginas/PaginaCatalogo.jsx';
import PaginaFichaBicicleta from './paginas/PaginaFichaBicicleta.jsx';
import PaginaEstaciones from './paginas/PaginaEstaciones.jsx';
import PaginaDetalleEstacion from './paginas/PaginaDetalleEstacion.jsx';
import PaginaReservas from './paginas/PaginaReservas.jsx';
import PaginaNuevaReserva from './paginas/PaginaNuevaReserva.jsx';
import PaginaAcceso from './paginas/PaginaAcceso.jsx';
import PaginaTaller from './paginas/PaginaTaller.jsx';
import PaginaNoEncontrada from './paginas/PaginaNoEncontrada.jsx';
export const router = createBrowserRouter([
{ path: '/', element: <PaginaCatalogo /> },
{ path: '/bicicletas/:bicicletaId', element: <PaginaFichaBicicleta /> },
{ path: '/estaciones', element: <PaginaEstaciones /> },
{ path: '/estaciones/:estacionId', element: <PaginaDetalleEstacion /> },
{ path: '/reservas', element: <PaginaReservas /> },
{ path: '/reservas/nueva', element: <PaginaNuevaReserva /> },
{ path: '/acceso', element: <PaginaAcceso /> },
{ path: '/taller', element: <PaginaTaller /> },
{ path: '*', element: <PaginaNoEncontrada /> }
]);Desmenucemos las decisiones:
- El fichero es
.jsx, no.js. Contiene JSX en la propiedadelement, así que la extensión debe permitirlo o Vite se quejará. pathes el patrón de URL, con/inicial en las rutas de primer nivel. Los segmentos que empiezan por:son dinámicos.elementrecibe un elemento de React ya creado (<PaginaCatalogo />), no el componente (PaginaCatalogo). Es una diferencia sutil pero estricta: pasar el componente sin instanciar es un error clásico y produce una pantalla en blanco.- Se exporta
routercon nombre, siguiendo la convención del proyecto:export defaultpara componentes, exportación nombrada para todo lo demás. createBrowserRouterse llama una sola vez, fuera de cualquier componente. Si lo llamases dentro de un componente, cada render crearía un enrutador nuevo y perderías todo el estado de navegación.- La ruta
*va al final por legibilidad, no por necesidad: el orden ya no decide nada (apartado 13).
Todavía no hay anidamiento. Diseno aún no está en el mapa, así que las pantallas se pintan sueltas, sin cabecera ni pie. Es un estado intermedio deliberado: lo arreglarás en 06-03, que es justo la lección de rutas anidadas.
- Montar el enrutador en
main.jsx
main.jsxRecuerda cómo estaba el punto de entrada al final del módulo 5:
// src/main.jsx — ANTES
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import LimiteDeError from './componentes/LimiteDeError.jsx';
import { ProveedorTema } from './contextos/ContextoTema.jsx';
import { ProveedorUsuario } from './contextos/ContextoUsuario.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}>
<ProveedorTema>
<ProveedorUsuario>
<App />
</ProveedorUsuario>
</ProveedorTema>
</LimiteDeError>
</StrictMode>
);El cambio consiste en sustituir <App /> por <RouterProvider router={router} />:
// src/main.jsx — DESPUÉS
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 { ProveedorTema } from './contextos/ContextoTema.jsx';
import { ProveedorUsuario } from './contextos/ContextoUsuario.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}>
<ProveedorTema>
<ProveedorUsuario>
<RouterProvider router={router} />
</ProveedorUsuario>
</ProveedorTema>
</LimiteDeError>
</StrictMode>
);Puntos que merecen comentario:
RouterProviderno aceptachildren. Todo lo que se pinta debajo sale del mapa de rutas. Si escribes<RouterProvider router={router}><Algo /></RouterProvider>, ese<Algo />no aparecerá nunca.App.jsxya no se importa. Puedes borrarlo o dejarlo vacío; su papel lo cumplen ahorarutas.jsx(qué se pinta) yDiseno(cómo se enmarca, a partir de 06-03).StrictModesigue en la raíz, como desde 01-03. Seguirá provocando el doble montaje en desarrollo, y eso incluye los efectos de las páginas: nada nuevo respecto a 05-02.ProveedorReservasyProveedorAvisosno aparecen aquí. En 06-03 los colocarás dentro deDiseno, la ruta raíz, junto al resto del marco de la aplicación. También podrías subirlos amain.jsx; la decisión se justifica en la lección siguiente.
Con esto ya puedes arrancar npm run dev, escribir http://localhost:5173/estaciones en la barra de direcciones y ver el listado de estaciones. Es el primer momento del curso en que la URL significa algo.
- Por qué los proveedores van fuera del enrutador
El orden StrictMode > LimiteDeError > ProveedorTema > ProveedorUsuario > RouterProvider no es arbitrario. Cada capa envuelve a la siguiente por una razón concreta:
flowchart TD
SM["StrictMode<br/><i>comprobaciones de desarrollo</i>"] --> LE["LimiteDeError<br/><i>captura fallos de TODO,<br/>incluido el enrutador</i>"]
LE --> PT["ProveedorTema<br/><i>sobrevive a los cambios de ruta</i>"]
PT --> PU["ProveedorUsuario<br/><i>la sesión NO se pierde al navegar</i>"]
PU --> RP["RouterProvider<br/><i>decide qué pantalla pintar</i>"]
RP --> PAG["La página activa<br/><i>cambia con la URL</i>"]
style RP fill:#e0f2fe
style PAG fill:#dcfce7
El argumento clave: lo que está por encima del enrutador no se desmonta al navegar. Si ProveedorUsuario estuviera dentro de una ruta, cambiar de / a /estaciones desmontaría el proveedor, su useState se reiniciaría y el usuario perdería la sesión en cada clic del menú. Lo mismo con el tema: pasarías de oscuro a claro cada vez que cambiases de pantalla.
En forma de tabla, el criterio para decidir dónde colocar cada proveedor:
| Situación | Dónde va |
|---|---|
| El estado debe sobrevivir a toda la vida de la aplicación (sesión, tema) | Fuera del enrutador, en main.jsx |
| El estado es del marco visual y lo consumen varias pantallas (avisos, reservas) | En la ruta raíz (Diseno), 06-03 |
| El estado solo interesa a una pantalla (término de búsqueda del catálogo) | Dentro de esa página |
Y el LimiteDeError global por encima de todo tiene un motivo adicional: si el propio enrutador lanzara un error —una ruta mal configurada, un element que no es un elemento—, alguien tiene que capturarlo. En 06-03 verás que además existe errorElement, que actúa dentro del enrutador y por rama de ruta; los dos mecanismos conviven y se reparten el trabajo.
- Enlaces:
<Link> frente a <a href>
<Link> frente a <a href>Momento de saldar la deuda del módulo 2. Recordemos la Cabecera tal como lleva escrita desde entonces:
// src/componentes/Cabecera.jsx — la versión con la deuda
<nav>
<a href="#catalogo">Catálogo</a>
<a href="#estaciones">Estaciones</a>
<a href="#reservas">Mis reservas</a>
</nav>La tentación es sustituir #catalogo por /:
Y es un error, aunque «funcione». Compara lo que ocurre con cada uno:
<a href="/estaciones"> |
<Link to="/estaciones"> |
|
|---|---|---|
| ¿Qué HTML produce? | <a href="/estaciones"> |
<a href="/estaciones"> (¡el mismo!) |
| Al hacer clic | El navegador descarta la aplicación y pide la página al servidor | Intercepta el clic, pushState y repinta |
| Recarga completa | Sí | No |
| Estado de React | Se pierde todo: sesión, tema, reservas, avisos | Intacto |
| Tiempo | Cientos de milisegundos, con pantalla en blanco | Instantáneo |
| Clic con Ctrl / botón central | Abre en pestaña nueva ✅ | Abre en pestaña nueva ✅ |
| Buscadores y lectores de pantalla | Lo ven como un enlace ✅ | Lo ven como un enlace ✅ |
Ese detalle de las dos últimas filas es importante y suele sorprender: <Link> renderiza un <a> real con su href. No es un <div> con un onClick, error habitual de quien se fabrica sus propios enlaces. Por eso conserva todo lo bueno de un enlace HTML —abrir en pestaña nueva, copiar dirección, indexación, navegación por teclado, anuncio correcto en un lector de pantalla, como exigía 03-06— y solo cambia el comportamiento del clic simple, que intercepta con preventDefault().
La regla es sencilla:
- Navegación dentro de la aplicación →
<Link to="…">. - Enlace a otro sitio web, a un PDF o a
mailto:→<a href="…">de toda la vida. React Router no debe interceptarlo.
// Correcto: interno con Link, externo con a
<Link to="/estaciones">Estaciones</Link>
<a href="https://www.ciclourbano.test/ayuda" target="_blank" rel="noreferrer">
Centro de ayuda
</a>Un aviso que ahorra horas de depuración: si en algún momento ves que al pulsar un enlace se pierde la sesión o parpadea toda la pantalla, casi seguro que has dejado un <a href> interno sin convertir. Es el fallo más repetido al enrutar una aplicación existente.
<NavLink> y la clase activa: la nav de Cabecera
<NavLink> y la clase activa: la nav de CabeceraPara un menú de navegación hace falta algo más: marcar visualmente cuál es la pantalla actual. <NavLink> es un <Link> que sabe si su destino coincide con la URL activa.
// src/componentes/Cabecera.jsx — la deuda del módulo 2, saldada
import { NavLink } from 'react-router';
import MenuUsuario from './MenuUsuario.jsx';
import estilos from './Cabecera.module.css';
function Cabecera() {
// Función que decide la clase según el estado del enlace
const clasesEnlace = ({ isActive }) =>
isActive ? `${estilos.enlace} ${estilos.activo}` : estilos.enlace;
return (
<header className={estilos.cabecera}>
<NavLink to="/" className={estilos.marca}>
CicloUrbano
</NavLink>
<nav aria-label="Navegación principal">
<NavLink to="/" end className={clasesEnlace}>
Catálogo
</NavLink>
<NavLink to="/estaciones" className={clasesEnlace}>
Estaciones
</NavLink>
<NavLink to="/reservas" className={clasesEnlace}>
Mis reservas
</NavLink>
</nav>
<MenuUsuario />
</header>
);
}
export default Cabecera;Tres cosas que explicar:
className puede ser una función. NavLink la llama con un objeto { isActive, isPending, isTransitioning } y usa lo que devuelva. Es la única prop de todo el proyecto que se comporta así, y merece la pena recordar por qué: el componente no sabe qué nombres de clase usa tu CSS Module, así que te deja decidir a ti. Lo mismo vale para style y para children:
{/* children como función: útil para añadir un icono solo al activo */}
<NavLink to="/reservas">
{({ isActive }) => (
<>
{isActive && <span aria-hidden="true">▸ </span>}
Mis reservas
</>
)}
</NavLink>La prop end no es opcional en el enlace al catálogo. Por defecto, un NavLink se considera activo si la URL empieza por su destino. Como todas las rutas empiezan por /, el enlace «Catálogo» aparecería marcado siempre, incluso estando en /estaciones. end exige coincidencia exacta y resuelve el problema:
| URL actual | <NavLink to="/"> |
<NavLink to="/" end> |
<NavLink to="/estaciones"> |
|---|---|---|---|
/ |
activo | activo | inactivo |
/estaciones |
activo ❌ | inactivo ✅ | activo |
/estaciones/est-02 |
activo ❌ | inactivo ✅ | activo (deseable) |
Fíjate en la última fila: para el enlace «Estaciones» sí queremos el comportamiento por defecto, porque estando en el detalle de una estación tiene sentido que la sección siga marcada. Por eso end va solo en el primero.
Accesibilidad. Marcar el enlace activo solo con color no basta (03-06). NavLink añade automáticamente aria-current="page" al elemento activo, lo que un lector de pantalla anuncia como «página actual». Aprovecha ese atributo también en el CSS, para no depender de una clase:
/* src/componentes/Cabecera.module.css */
.enlace {
color: var(--color-texto);
text-decoration: none;
padding: calc(var(--espacio) / 2) var(--espacio);
border-radius: var(--radio);
}
.enlace:hover {
background: var(--color-borde);
}
.activo,
.enlace[aria-current='page'] {
color: var(--color-marca);
font-weight: 700;
/* Además del color, un indicador no cromático */
box-shadow: inset 0 -2px 0 var(--color-marca);
}
- Segmentos dinámicos y
useParams
useParamsLa ruta /bicicletas/:bicicletaId tiene un segmento dinámico: los dos puntos indican que ese trozo es variable y su valor debe capturarse.
| URL | ¿Coincide con /bicicletas/:bicicletaId? |
useParams() devuelve |
|---|---|---|
/bicicletas/bici-003 |
Sí | { bicicletaId: 'bici-003' } |
/bicicletas/42 |
Sí | { bicicletaId: '42' } |
/bicicletas |
No (falta el segmento) | — |
/bicicletas/bici-003/piezas |
No (sobra un segmento) | — |
Así se lee en la página:
// src/paginas/PaginaFichaBicicleta.jsx
import { useParams, Link } from 'react-router';
import { bicicletas, estaciones } from '../datos/dominio.js';
import EtiquetaEstado from '../componentes/EtiquetaEstado.jsx';
import PaginaNoEncontrada from './PaginaNoEncontrada.jsx';
function PaginaFichaBicicleta() {
const { bicicletaId } = useParams();
const bicicleta = bicicletas.find((bici) => bici.id === bicicletaId);
// El parámetro puede ser cualquier cosa: la URL la escribe el usuario
if (!bicicleta) {
return <PaginaNoEncontrada recurso="bicicleta" identificador={bicicletaId} />;
}
const estacion = estaciones.find((est) => est.id === bicicleta.estacionId);
return (
<article>
<h2>{bicicleta.modelo}</h2>
<EtiquetaEstado estado={bicicleta.estado} />
<dl>
<dt>Tipo</dt>
<dd>{bicicleta.tipo}</dd>
<dt>Estación</dt>
<dd>
<Link to={`/estaciones/${bicicleta.estacionId}`}>
{estacion ? estacion.nombre : 'Sin asignar'}
</Link>
</dd>
<dt>Precio por hora</dt>
<dd>{bicicleta.precioHora.toFixed(2)} €</dd>
</dl>
<Link to="/">Volver al catálogo</Link>
</article>
);
}
export default PaginaFichaBicicleta;Y el enlace que lleva hasta aquí, desde TarjetaBicicleta:
// src/componentes/TarjetaBicicleta.jsx — fragmento
<h3>
<Link to={`/bicicletas/${bicicleta.id}`}>{bicicleta.modelo}</Link>
</h3>La conversión de tipos es responsabilidad tuya. Este es el punto que más fallos silenciosos provoca:
Todos los parámetros de ruta son cadenas de texto. Siempre. La URL es texto; React Router no puede saber si
42es un número, un código postal o un identificador.
En CicloUrbano no molesta, porque los identificadores ya son cadenas (bici-003). Pero si tu API usara identificadores numéricos, esto fallaría de forma desconcertante:
const { bicicletaId } = useParams(); // '3', una CADENA
bicicletas.find((b) => b.id === bicicletaId); // ❌ 3 === '3' es false → undefined
bicicletas.find((b) => b.id == bicicletaId); // ⚠️ funciona, pero == es mala idea
bicicletas.find((b) => b.id === Number(bicicletaId)); // ✅ conversión explícitaConviértelo cuanto antes y en un solo sitio, y valida el resultado:
const { bicicletaId } = useParams();
const idNumerico = Number(bicicletaId);
if (!Number.isInteger(idNumerico) || idNumerico <= 0) {
return <PaginaNoEncontrada recurso="bicicleta" identificador={bicicletaId} />;
}
- Qué hacer cuando el identificador no existe
Cualquiera puede escribir /bicicletas/bici-999 en la barra de direcciones, o llegar desde un enlace antiguo a una bicicleta dada de baja. Tienes tres estrategias, y elegir bien importa:
| Estrategia | Cómo | Cuándo conviene |
|---|---|---|
| Pintar el «no encontrado» en su sitio | if (!bicicleta) return <PaginaNoEncontrada … /> |
Recomendada. La URL se conserva, el usuario puede corregirla y el enlace sigue siendo compartible para depurar |
| Redirigir a la lista | <Navigate to="/" replace /> |
Cuando el detalle no tiene sentido sin contexto y la lista es una alternativa útil. Se ve en 06-04 |
| Lanzar y dejar que lo capture el enrutador | throw new Response('No encontrada', { status: 404 }) + errorElement |
Aplicaciones grandes con manejo de errores centralizado. Se ve en 06-03 |
Amplía PaginaNoEncontrada para que sirva a los dos usos —URL inexistente y recurso inexistente— con un mensaje afinado:
// src/paginas/PaginaNoEncontrada.jsx
import { Link, useLocation } from 'react-router';
import Aviso from '../componentes/Aviso.jsx';
/**
* Pantalla de «no encontrado» de CicloUrbano.
* Props:
* - recurso (cadena, opcional): 'bicicleta', 'estación'… Si falta, es una URL desconocida
* - identificador (cadena, opcional): el id que no se ha encontrado
*/
function PaginaNoEncontrada({ recurso, identificador }) {
const { pathname } = useLocation();
const titulo = recurso
? `No existe ninguna ${recurso} con el identificador «${identificador}»`
: 'Esta página no existe';
return (
<section>
<Aviso tono="error" titulo={titulo}>
<p>
{recurso
? 'Puede que se haya dado de baja o que el enlace esté anticuado.'
: `La dirección ${pathname} no corresponde a ninguna pantalla de CicloUrbano.`}
</p>
</Aviso>
<p>
<Link to="/">Ir al catálogo</Link> · <Link to="/estaciones">Ver estaciones</Link>
</p>
</section>
);
}
export default PaginaNoEncontrada;Una precisión honesta que conviene conocer: esta pantalla no devuelve un código HTTP 404. El servidor ha entregado el index.html con estado 200 y el 404 es solo visual. Para que un buscador reciba el código correcto hace falta renderizado en servidor (10-01). En una aplicación interna como el panel de CicloUrbano no es un problema; en una web pública, sí.
- Parámetros de consulta con
useSearchParams
useSearchParamsVuelve a PaginaCatalogo. El filtro por tipo vive hoy en un useState, y eso tiene el mismo defecto que denunciábamos en 06-01: si un operario filtra por electrica y le pasa la dirección a un compañero, este ve el catálogo sin filtrar. El filtro es información sobre lo que el usuario está mirando: pertenece a la URL.
useSearchParams es a los parámetros de consulta lo que useState es al estado local, con una firma deliberadamente parecida:
Con la diferencia de que parametros es un objeto URLSearchParams estándar del navegador, no un objeto plano. Sus métodos:
| Método | Qué hace |
|---|---|
parametros.get('tipo') |
Devuelve el valor, o null si no está |
parametros.getAll('tipo') |
Todos los valores, si la clave se repite |
parametros.has('tipo') |
true / false |
parametros.set('tipo', 'carga') |
Fija el valor (sobre una copia, ver abajo) |
parametros.delete('tipo') |
Elimina la clave |
parametros.toString() |
"tipo=carga&orden=precio" |
Así queda el catálogo:
// src/paginas/PaginaCatalogo.jsx — con el filtro en la URL
import { useState } from 'react';
import { useSearchParams } from 'react-router';
import BuscadorBicicletas from '../componentes/BuscadorBicicletas.jsx';
import SelectorTipo from '../componentes/SelectorTipo.jsx';
import ListaBicicletas from '../componentes/ListaBicicletas.jsx';
import { bicicletas, estaciones } from '../datos/dominio.js';
function PaginaCatalogo() {
const [termino, setTermino] = useState('');
const [parametros, establecerParametros] = useSearchParams();
// La URL manda: si no hay ?tipo=, el filtro es 'todos'
const tipoElegido = parametros.get('tipo') ?? 'todos';
function manejarCambioDeTipo(nuevoTipo) {
// Se parte SIEMPRE de los parámetros actuales para no perder los demás
const siguientes = new URLSearchParams(parametros);
if (nuevoTipo === 'todos') {
siguientes.delete('tipo'); // sin filtro, URL limpia: '/' y no '/?tipo=todos'
} else {
siguientes.set('tipo', nuevoTipo);
}
// replace: cambiar de filtro no debería llenar el historial de entradas
establecerParametros(siguientes, { replace: true });
}
const visibles = bicicletas
.filter((bici) => tipoElegido === 'todos' || bici.tipo === tipoElegido)
.filter((bici) => bici.modelo.toLowerCase().includes(termino.toLowerCase()));
return (
<section>
<h2>Catálogo de bicicletas</h2>
<BuscadorBicicletas termino={termino} alCambiarTermino={setTermino} />
<SelectorTipo tipoElegido={tipoElegido} alCambiarTipo={manejarCambioDeTipo} />
{visibles.length === 0 ? (
<AvisoSinResultados termino={termino} tipo={tipoElegido} />
) : (
<ListaBicicletas bicicletas={visibles} estaciones={estaciones} />
)}
</section>
);
}
export default PaginaCatalogo;Lo esencial de este código:
SelectorTipono se ha tocado. Sigue siendo el componente controlado de 03-04, con sus propstipoElegidoyalCambiarTipo. Lo único que ha cambiado es de dónde sale el valor y a dónde va el cambio. Eso es exactamente lo que buscábamos al mantener los componentes desacoplados del enrutador (apartado 3).- Se copia antes de modificar:
new URLSearchParams(parametros). Mutar el objeto que devuelve el hook no repinta nada y puede provocar incoherencias, por la misma razón de inmutabilidad de 05-01. ?tipo=todosse elimina en vez de escribirse. Un valor por defecto no debe ensuciar la URL;/y/?tipo=todosmostrarían lo mismo y son dos direcciones distintas, malo para compartir y para la analítica.{ replace: true }evita que cada clic en el filtro añada una entrada al historial. Si un usuario prueba cinco tipos, no querrá pulsar «atrás» cinco veces para salir del catálogo. Es un juicio de diseño: para una paginación sí suele preferirse dejar rastro.- El término de búsqueda sigue en
useState. Decisión deliberada: se teclea letra a letra y llevarlo a la URL generaría decenas de entradas o exigiría combinarlo con eluseDebouncede 05-06. Cuando el buscador tenga que ser compartible, el patrón seráuseSearchParams+useDebounce.
Qué se gana exactamente al convertir estado de React en estado de la URL:
Antes (useState) |
Después (useSearchParams) |
|---|---|
/ siempre, filtre lo que filtre |
/?tipo=electrica |
| Compartir el enlace pierde el filtro | El compañero ve exactamente lo mismo |
| F5 devuelve al catálogo sin filtrar | F5 conserva el filtro |
| «Atrás» sale de la aplicación | «Atrás» vuelve al filtro anterior (si no usas replace) |
| La analítica no distingue filtros | Se puede medir qué tipo se consulta más |
| El estado inicial hay que inventarlo | Lo dicta la URL |
Y la contrapartida honesta: la URL es pública y compartible, así que ahí solo debe ir lo que no importe que se vea y se guarde. Filtros, ordenaciones, número de página, pestaña activa: sí. Datos personales, contenido de un formulario o cualquier secreto: nunca.
- La ruta comodín y la página de no encontrado
La última entrada del mapa es:
El asterisco es la ruta comodín (splat route): coincide con cualquier URL que ninguna otra ruta haya reclamado. Su papel es que un error tipográfico produzca una pantalla útil en vez de un vacío. Sin ella, /estacionez renderizaría nada en absoluto: pantalla en blanco, sin ningún error en consola. Es un fallo desconcertante y muy común olvidar esa línea.
El valor capturado por el comodín se lee con la clave '*', útil si quieres mostrarlo o registrarlo:
Un comodín también sirve en un punto intermedio, no solo en la raíz. { path: '/documentacion/*' } captura cualquier profundidad bajo ese prefijo, patrón habitual al integrar contenido externo. En CicloUrbano no hace falta.
- Cómo elige React Router la mejor coincidencia
Pregunta natural: si la URL es /reservas/nueva, ¿por qué no coincide con /reservas o con *? La respuesta es una de las mejores decisiones de diseño de React Router desde la v6:
React Router puntúa todas las rutas y elige la más específica, sin importar en qué orden las hayas escrito en el array.
El criterio de puntuación, simplificado:
| Tipo de segmento | Puntuación | Ejemplo |
|---|---|---|
| Segmento literal | Alta | estaciones |
| Segmento dinámico | Media | :estacionId |
Comodín * |
Mínima | * |
Aplicado a /estaciones/est-02, con el mapa completo:
| Ruta candidata | ¿Coincide? | Puntuación | Resultado |
|---|---|---|---|
/estaciones/:estacionId |
Sí | literal + dinámico → alta | ✅ Gana |
/estaciones |
No, sobra un segmento | — | Descartada |
* |
Sí | mínima | Perdedora |
Compáralo con React Router v5 y anteriores, donde <Switch> tomaba la primera ruta que coincidiera en orden de escritura. Aquello obligaba a ordenar el array de más específico a más general, a usar exact por todas partes y producía errores muy difíciles de ver: una ruta colocada dos líneas más arriba de la cuenta se «comía» a todas las demás. Hoy puedes escribir el mapa en el orden que te resulte legible.
Dos matices que conviene retener:
- La coincidencia debe ser completa.
/estacionesno coincide con la URL/estaciones/est-02a menos que tenga rutas hijas que absorban el resto (06-03) o termine en*. endenNavLinkes otra cosa. Gobierna solo la clase activa del enlace, no qué ruta se pinta. Es normal confundirlos al principio.
flowchart TD
URL["URL: /estaciones/est-02"] --> CAND{"Rutas candidatas"}
CAND --> R1["/estaciones/:estacionId<br/>literal + dinámico"]
CAND --> R2["/estaciones<br/>❌ no cubre toda la URL"]
CAND --> R3["*<br/>puntuación mínima"]
R1 --> GANA["✅ Se renderiza<br/>PaginaDetalleEstacion<br/>params: { estacionId: 'est-02' }"]
style GANA fill:#dcfce7
style R2 fill:#fecaca
Errores Comunes y Consejos
Pasar el componente en vez del elemento. { path: '/', element: PaginaCatalogo } en lugar de element: <PaginaCatalogo />. No lanza un error claro: simplemente no se pinta nada. Recuerda que element recibe JSX ya creado.
Dejar <a href> internos sin convertir. El síntoma es inconfundible: al pulsar un enlace, la pantalla parpadea y se pierden la sesión, el tema y las reservas. Busca href="/ en tu proyecto y convierte todo lo interno a <Link>.
Olvidar end en el NavLink del inicio. El enlace «Catálogo» queda marcado como activo en todas las pantallas. Lo mismo pasa con cualquier NavLink cuyo destino sea prefijo de otras rutas.
Llamar a createBrowserRouter dentro de un componente. Cada render crearía un enrutador nuevo, y la aplicación volvería a la ruta inicial en cuanto algo repintara. El mapa se crea una vez, en el ámbito del módulo.
Mutar el objeto de useSearchParams. parametros.set('tipo', 'carga') sobre el objeto devuelto por el hook no provoca ningún repintado. Copia siempre: new URLSearchParams(parametros).
Poner un ? de más al construir la URL. establecerParametros('?tipo=carga') produce ??tipo=carga. Pasa un URLSearchParams o un objeto plano y deja que React Router ponga el separador.
Esperar un número en useParams. Todo es cadena. Convierte y valida, en un solo sitio y cuanto antes.
Olvidar la ruta *. Cualquier error tipográfico produce una pantalla en blanco silenciosa, sin error en consola. Es de los primeros fallos que un usuario reporta.
Consejo: nombra los parámetros igual en la ruta y en el código. path: '/bicicletas/:bicicletaId' y const { bicicletaId } = useParams(). Si la ruta dice :id y desestructuras bicicletaId, obtendrás undefined sin ningún aviso.
Consejo: mantén una única fuente de verdad por dato. Si el tipo elegido vive en la URL, no lo dupliques en un useState «para tenerlo a mano». Duplicarlo garantiza que antes o después se desincronicen, exactamente como advertía 05-01 sobre el estado derivado.
Consejo: comprueba la recarga en cada ruta nueva. Navegar con enlaces siempre funciona; recargar es lo que descubre los problemas de configuración del servidor. Con Vite en desarrollo no verás el fallo, pero conviene tener presente lo visto en 06-01 sobre la reescritura a index.html.
Ejercicios
Ejercicio 1: la ruta de detalle de estación
Añade a CicloUrbano la pantalla de detalle de estación, /estaciones/:estacionId. Debe:
- Leer el parámetro y buscar la estación en
estaciones. - Mostrar nombre, barrio y plazas.
- Listar las bicicletas cuyo
estacionIdcoincida, cada una enlazando a su ficha. - Mostrar el «no encontrado» si el identificador no existe.
- Incluir un enlace de vuelta a
/estaciones.
Añade además el enlace desde TarjetaEstacion para llegar hasta ahí.
Ejercicio 2: ordenar el listado desde la URL
En PaginaEstaciones, añade un desplegable que permita ordenar las estaciones por nombre o por plazas, guardando la elección en un parámetro de consulta orden. Requisitos:
- La URL debe quedar
/estaciones?orden=plazas. - El valor por defecto (
nombre) no debe aparecer en la URL. - No se deben perder otros parámetros de consulta que pudiera haber.
- Un valor inválido escrito a mano (
?orden=inventado) no debe romper la pantalla.
Ejercicio 3: detectar los fallos
Este mapa de rutas y esta cabecera tienen cinco errores. Encuéntralos y corrígelos.
// src/rutas.jsx
import { createBrowserRouter } from 'react-router';
import PaginaCatalogo from './paginas/PaginaCatalogo.jsx';
import PaginaFichaBicicleta from './paginas/PaginaFichaBicicleta.jsx';
import PaginaEstaciones from './paginas/PaginaEstaciones.jsx';
function crearRouter() {
return createBrowserRouter([
{ path: '/', element: PaginaCatalogo },
{ path: '/bicicletas/:id', element: <PaginaFichaBicicleta /> },
{ path: '/estaciones', element: <PaginaEstaciones /> }
]);
}
export default crearRouter;// src/componentes/Cabecera.jsx
import { NavLink } from 'react-router';
function Cabecera() {
return (
<nav>
<NavLink to="/" className={({ isActive }) => (isActive ? 'activo' : '')}>
Catálogo
</NavLink>
<a href="/estaciones">Estaciones</a>
</nav>
);
}// src/paginas/PaginaFichaBicicleta.jsx — fragmento
const { bicicletaId } = useParams();
const bicicleta = bicicletas.find((bici) => bici.id === bicicletaId);
return <h2>{bicicleta.modelo}</h2>;Soluciones
Solución 1
// src/paginas/PaginaDetalleEstacion.jsx
import { useParams, Link } from 'react-router';
import { estaciones, bicicletas } from '../datos/dominio.js';
import EtiquetaEstado from '../componentes/EtiquetaEstado.jsx';
import PaginaNoEncontrada from './PaginaNoEncontrada.jsx';
function PaginaDetalleEstacion() {
const { estacionId } = useParams();
const estacion = estaciones.find((est) => est.id === estacionId);
if (!estacion) {
return <PaginaNoEncontrada recurso="estación" identificador={estacionId} />;
}
const flota = bicicletas.filter((bici) => bici.estacionId === estacion.id);
return (
<article>
<h2>{estacion.nombre}</h2>
<p>
Barrio de {estacion.barrio} · {estacion.plazas} plazas ·{' '}
{flota.length} bicicletas asignadas
</p>
<h3>Flota de la estación</h3>
{flota.length === 0 ? (
<p>No hay bicicletas asignadas a esta estación.</p>
) : (
<ul>
{flota.map((bici) => (
<li key={bici.id}>
<Link to={`/bicicletas/${bici.id}`}>{bici.modelo}</Link>{' '}
<EtiquetaEstado estado={bici.estado} />
</li>
))}
</ul>
)}
<Link to="/estaciones">Volver a las estaciones</Link>
</article>
);
}
export default PaginaDetalleEstacion;// src/componentes/TarjetaEstacion.jsx — fragmento con el enlace añadido
<h3>
<Link to={`/estaciones/${estacion.id}`}>{estacion.nombre}</Link>
</h3>Con la ruta ya presente en rutas.jsx, no hay nada más que registrar. Y observa que /estaciones y /estaciones/est-02 conviven sin problema gracias a la puntuación del apartado 13: el orden en el array es irrelevante.
Solución 2
// src/paginas/PaginaEstaciones.jsx
import { useSearchParams } from 'react-router';
import TarjetaEstacion from '../componentes/TarjetaEstacion.jsx';
import { estaciones, bicicletas } from '../datos/dominio.js';
const ORDENES_VALIDOS = ['nombre', 'plazas'];
const ORDEN_POR_DEFECTO = 'nombre';
function PaginaEstaciones() {
const [parametros, establecerParametros] = useSearchParams();
// Validación: un valor inventado en la URL cae al valor por defecto
const ordenBruto = parametros.get('orden');
const orden = ORDENES_VALIDOS.includes(ordenBruto) ? ordenBruto : ORDEN_POR_DEFECTO;
function manejarCambioDeOrden(evento) {
const nuevo = evento.target.value;
const siguientes = new URLSearchParams(parametros); // copia: conserva lo demás
if (nuevo === ORDEN_POR_DEFECTO) {
siguientes.delete('orden'); // el valor por defecto no ensucia la URL
} else {
siguientes.set('orden', nuevo);
}
establecerParametros(siguientes, { replace: true });
}
// Copia antes de ordenar: sort() muta el array original (05-01)
const ordenadas = [...estaciones].sort((a, b) =>
orden === 'plazas' ? b.plazas - a.plazas : a.nombre.localeCompare(b.nombre)
);
return (
<section>
<h2>Estaciones</h2>
<p>
<label htmlFor="orden-estaciones">Ordenar por</label>{' '}
<select id="orden-estaciones" value={orden} onChange={manejarCambioDeOrden}>
<option value="nombre">Nombre (A-Z)</option>
<option value="plazas">Plazas (mayor primero)</option>
</select>
</p>
<ul>
{ordenadas.map((estacion) => (
<li key={estacion.id}>
<TarjetaEstacion
estacion={estacion}
bicicletasEnEstacion={bicicletas.filter((b) => b.estacionId === estacion.id)}
/>
</li>
))}
</ul>
</section>
);
}
export default PaginaEstaciones;Los cuatro requisitos quedan cubiertos por: la lista blanca ORDENES_VALIDOS (validación), el delete cuando el valor es el de por defecto (URL limpia), la copia new URLSearchParams(parametros) (no perder otros parámetros) y el [...estaciones] antes de sort (no mutar los datos originales). El <select> es un componente controlado de 03-04, con la particularidad de que su valor sale de la URL, no de un useState.
Solución 3
Los cinco errores:
element: PaginaCatalogopasa el componente, no el elemento. Debe serelement: <PaginaCatalogo />. Síntoma: pantalla en blanco sin error claro.createBrowserRouterdentro de una función que se exporta comocrearRouter. Simain.jsxla llama en cada render, se crea un enrutador nuevo cada vez. Debe crearse una sola vez en el ámbito del módulo y exportarse con nombre:export const router = createBrowserRouter([...]);.- Falta la ruta comodín
{ path: '*', element: <PaginaNoEncontrada /> }. Cualquier URL desconocida deja la pantalla vacía en silencio. - Desajuste de nombres: la ruta declara
:idpero la página desestructurabicicletaId, que seráundefined. Deben coincidir; lo correcto según el mapa del módulo es:bicicletaIden ambos sitios. - En
Cabecera,<a href="/estaciones">provoca una recarga completa y pierde todo el estado de React. Debe ser<NavLink to="/estaciones">.
Y un sexto fallo, latente, que conviene señalar: en PaginaFichaBicicleta no se comprueba si bicicleta existe, así que /bicicletas/bici-999 reventará con «Cannot read properties of undefined (reading 'modelo')». Añade la guarda del apartado 10. Nota, además, que ese error sí lo capturaría el LimiteDeError global de 04-05, pero tumbando la aplicación entera para algo que es simplemente una URL mal escrita: manejarlo en su sitio es mucho mejor experiencia.
Conclusión
CicloUrbano ya está enrutada. Has instalado react-router —el paquete de la v7, sabiendo que react-router-dom es lo que verás en proyectos de la v6—, has creado src/rutas.jsx con el mapa completo como array de objetos y has montado <RouterProvider router={router} /> en main.jsx conservando el orden StrictMode > LimiteDeError > ProveedorTema > ProveedorUsuario, porque lo que está por encima del enrutador no se desmonta al navegar y por eso la sesión y el tema sobreviven a los cambios de pantalla. El proyecto tiene una carpeta src/paginas/ con una pantalla por ruta y una regla clara: las páginas pueden depender del enrutador; los componentes reutilizables reciben sus datos por props y siguen siendo probables por separado.
Por el camino has saldado la deuda que arrastraba la Cabecera desde el módulo 2: los tres <a href="#…"> son ahora <NavLink> con clase activa, aria-current="page" gratis y la prop end en el enlace al catálogo para que no aparezca marcado en todas las pantallas. Sabes que <Link> produce un <a> real —con todo lo bueno de un enlace HTML— y solo intercepta el clic simple, mientras que un <a href> interno destruye la aplicación y se lleva por delante sesión, tema y reservas. Has leído el identificador de la ficha con useParams, recordando que todo parámetro de ruta es una cadena y que la conversión y la validación son tuyas, y has decidido qué hacer cuando el recurso no existe. Y has convertido el filtro del SelectorTipo de estado de React en estado de la URL con useSearchParams, ganando enlaces compartibles, recarga que conserva el filtro y analítica útil, sin tocar una sola línea del componente controlado. Cierran el cuadro la ruta comodín * —cuya ausencia produce pantallas en blanco silenciosas— y el sistema de puntuación que elige la coincidencia más específica sin depender del orden del array.
Queda un problema visible: las pantallas se pintan solas, sin cabecera ni pie, porque Diseno se quedó fuera del mapa. Reintroducirlo como un componente que envuelve a cada página funcionaría, pero volvería a montar la cabecera en cada navegación y no permitiría compartir datos entre marco y pantalla. La solución correcta es hacer de Diseno la ruta padre de todas las demás y abrir en él un hueco donde pintar la hija activa; con la misma idea construirás las pestañas flota e incidencias dentro del detalle de estación, las migas de pan y el manejo de errores por rama. La próxima lección es Rutas Anidadas.
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
