🧭 3.4 · React Router: navegación profesional en SPAs

⏱ 6h 00min ⚡ 100 XP 🏅 Mid Junior 📖 Frontend Moderno y Proyecto Final
"Una SPA sin routing es un PDF con botones. Con routing, es una aplicación real."

🎯 Objetivo del tema

Al terminar este tema serás capaz de: Al terminar este tema serás capaz de crear una Single Page Application con múltiples páginas usando React Router: rutas estáticas (/inicio, /about), rutas dinámicas (/pais/:id), navegación con Link y useNavigate, rutas protegidas (login requerido), y manejo de 404. Vas a aplicar todo a la app de países: la lista será la home, y al hacer clic en un país irás a su página de detalle con más info de REST Countries.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

🗺️ Mapa del tema

En el Tema 3.1 hiciste tu primera app de React: una SPA con un solo componente. Hoy le agregas navegación: tu app tendrá múltiples 'páginas' (rutas) sin recargar el navegador. React Router es la librería estándar: 60k estrellas en GitHub, usada por Netflix, Airbnb, Discord, Notion. Su magia: la URL cambia, pero la página NO se recarga. Es lo que hace que una app React se sienta como una 'app de verdad' (Gmail, Twitter, etc.) y no como un sitio web estático.

1. Por qué necesitas routing en una SPA

El problema del 'click y no pasa nada' sin React Router.

2. BrowserRouter, Route, y Switch (Routes)

Las 3 piezas fundamentales: el contenedor, las rutas, y el conmutador.

3. Link y useNavigate: navegación declarativa e imperativa

Cómo cambiar de página: con <Link> para links, useNavigate para código.

4. Rutas dinámicas: /pais/:id

Cómo pasar parámetros en la URL y leerlos con useParams.

5. Rutas protegidas: lógica de autenticación

Redirigir a /login si el usuario no está autenticado.

6. Proyecto real: app de países con páginas de detalle

Lista + click en país + página con más info + 404.

3.4.1 Por qué necesitas routing en una SPA

Sin React Router, una 'SPA' de React con varios componentes no tiene URLs: si el usuario hace clic en 'Ver detalle' y tú muestras un componente nuevo, la URL sigue siendo la misma. No puede compartir el link, no puede usar el botón 'Atrás' del navegador, y al recargar pierde el estado. React Router resuelve TODO: cambia la URL, mantiene el estado, permite compartir links, y soporta el botón 'Atrás' del navegador.

📷 Imagen referencial: React Router: el navegador pide /pais/arg → React Router detecta la ruta → renderiza el componente <DetallePais /> con useParams para leer 'arg'. Sin recargar la página.
  • 1. URL que refleja el estado: /pais/arg muestra Argentina, /pais/mex muestra México.
  • 2. Botón 'Atrás' del navegador funciona: vuelve a la lista sin recargar.
  • 3. Compartir links: puedes enviar https://mi-app.com/pais/arg a un amigo y ve Argentina.
  • 4. Bookmarks: el usuario puede guardar páginas específicas.
  • 5. SEO (con SSR): los crawlers de Google indexan cada página.
⭐ React Router v6: la versión actual: React Router NO es parte de React core. Es una librería externa (react-router-dom) que se instala con npm. Hay 2 versiones principales: v6 (la actual, recomendada) y v5 (legacy, muchos tutoriales viejos). En este tema uso v6, que es la estándar en 2024-2025. Si ves tutoriales con <Switch> o useHistory(), son de v5: actualízate a v6 con <Routes> y useNavigate.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

3.4.2 BrowserRouter, Route y Routes

React Router tiene 3 piezas fundamentales: BrowserRouter envuelve toda tu app y habilita el routing. Routes (antes Switch) es el conmutador que decide qué componente mostrar según la URL. Route define cada ruta individual: path es la URL, element es el componente a renderizar. Es como un sistema de semáforos: el BrowserRouter es el sistema de semáforos, las Routes son las luces, y cada Route es una luz específica.

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Inicio } from './pages/Inicio';
import { Sobre } from './pages/Sobre';
import { Contacto } from './pages/Contacto';
import { NoEncontrada } from './pages/NoEncontrada';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Inicio</Link>
        <Link to="/sobre">Sobre</Link>
        <Link to="/contacto">Contacto</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Inicio />} />
        <Route path="/sobre" element={<Sobre />} />
        <Route path="/contacto" element={<Contacto />} />
        <Route path="*" element={<NoEncontrada />} />  {/* 404 */}
      </Routes>
    </BrowserRouter>
  );
}

// Reglas de React Router v6:
// 1. BrowserRouter SIEMPRE arriba de Routes.
// 2. <Routes> contiene N <Route>.
// 3. La primera <Route> que coincide gana (orden importa).
// 4. path="*" es el catch-all (404) — va al FINAL.
// 5. element={...} recibe un JSX (no un component prop como en v5).

BrowserRouter + Routes + Route: el esqueleto mínimo

⭐ Orden de Routes: específicas primero: El orden de las Routes IMPORTA: la primera que coincide con la URL gana. Si pones path="/" ANTES que path="/pais/:id", NUNCA llegarás a la ruta dinámica porque / matchea todo. La regla: pon las rutas MÁS ESPECÍFICAS primero, y la genérica (/) al final (o usa path="*" para 404 al final). Es un error muy común.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

3.4.3 Link y useNavigate: navegación

Hay 2 formas de navegar en React Router: DECLARATIVA con <Link> (para links en el JSX) e IMPERATIVA con useNavigate (para navegación desde código, como después de un login exitoso). Es la misma distinción que en HTML: <a href> vs location.href. Link es declarativo, useNavigate es imperativo.

import { Link, useNavigate, useLocation } from 'react-router-dom';

// 1. Link: navegación declarativa (como <a> pero sin recargar)
function Navbar() {
  return (
    <nav>
      <Link to="/">Inicio</Link>
      <Link to="/sobre">Sobre mí</Link>
      <Link to="/contacto" className="active">Contacto</Link>
    </nav>
  );
}

// 2. useNavigate: navegación imperativa (desde código)
function LoginForm() {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  async function handleSubmit(e) {
    e.preventDefault();
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    if (response.ok) {
      navigate('/dashboard');  // redirige después del login exitoso
    } else {
      alert('Credenciales inválidas');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      <button type="submit">Iniciar sesión</button>
    </form>
  );
}

// 3. NavLink: Link con clase 'active' automática cuando coincide la URL
function Navbar() {
  return (
    <nav>
      <NavLink to="/inicio" className={({isActive}) => isActive ? 'active' : ''}>Inicio</NavLink>
      <NavLink to="/sobre" className={({isActive}) => isActive ? 'active' : ''}>Sobre</NavLink>
    </nav>
  );
}

// 4. useNavigate con replace: reemplaza la entrada del historial (no 'Atrás')
function LogoutButton() {
  const navigate = useNavigate();
  const logout = () => {
    localStorage.removeItem('token');
    navigate('/login', { replace: true });  // reemplaza, no agrega al historial
  };
  return <button onClick={logout}>Cerrar sesión</button>;
}

Link, useNavigate, NavLink: las 3 formas de navegar

⭐ <a> recarga, <Link> navega sin recargar: NUNCA uses <a href="/ruta"> en una SPA de React. Eso causa un RECARGA COMPLETA de la página, perdiendo el estado, los datos cargados, y toda la experiencia de SPA. SIEMPRE usa <Link to="/ruta"> de React Router: navegas sin recargar. Es uno de los errores más comunes de juniors que vienen de HTML clásico. La regla: <a> para ir a OTRO sitio web, <Link> para navegar DENTRO de tu SPA.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

3.4.4 Rutas dinámicas: /pais/:id

Las rutas dinámicas son las que tienen un PARÁMETRO variable: /pais/arg, /pais/mex, /pais/col. La parte variable es el :id, que se lee con useParams. Es el patrón para páginas de detalle: lista de productos → click en uno → /producto/:id. Es la forma estándar de hacer URLs amigables y compartibles en cualquier app.

import { useParams, useNavigate, Link } from 'react-router-dom';
import { useFetch } from '../hooks/useFetch';

// Página de detalle: recibe el id del país en la URL
function DetallePais() {
  const { id } = useParams();  // lee :id de la URL
  const navigate = useNavigate();
  const { data: pais, cargando, error } = useFetch(
    `https://restcountries.com/v3.1/alpha/${id}?fields=name,capital,population,flag,region,subregion,languages,currencies,borders`
  );

  if (cargando) return <p>Cargando país...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!pais) return <p>País no encontrado</p>;

  const p = Array.isArray(pais) ? pais[0] : pais;  // la API devuelve array o objeto

  return (
    <article className="pais-detalle">
      <button onClick={() => navigate(-1)}>← Volver</button>
      <img src={p.flag} alt={p.name.common} className="flag-grande" />
      <h1>{p.name.common} ({p.name.official})</h1>
      <dl>
        <dt>Capital</dt><dd>{p.capital?.[0]}</dd>
        <dt>Población</dt><dd>{p.population.toLocaleString()}</dd>
        <dt>Región</dt><dd>{p.region} / {p.subregion}</dd>
        <dt>Idiomas</dt><dd>{Object.values(p.languages || {}).join(', ')}</dd>
        <dt>Monedas</dt><dd>{Object.values(p.currencies || {}).map(c => c.name).join(', ')}</dd>
      </dl>

      <h2>Países fronterizos:</h2>
      <ul>
        {(p.borders || []).map(code => (
          <li key={code}><Link to={`/pais/${code}`}>{code}</Link></li>
        ))}
      </ul>
    </article>
  );
}

// En el App.jsx, defines la ruta dinámica:
<Routes>
  <Route path="/" element={<ListaPaises />} />
  <Route path="/pais/:id" element={<DetallePais />} />
  <Route path="*" element={<NoEncontrada />} />
</Routes>

// En la lista, el link al detalle:
function PaisCard({ pais }) {
  return (
    <Link to={`/pais/${pais.cca3}`} className="pais-card-link">
      <img src={pais.flags.png} alt={pais.name.common} />
      <h2>{pais.name.common}</h2>
    </Link>
  );
}

Ruta dinámica /pais/:id con useParams y REST Countries

⭐ Usa códigos cca3 en URLs, no nombres: La API REST Countries devuelve los países por código cca3 (alpha-3): 'ARG', 'MEX', 'COL'. Estos códigos son ÚNICOS y cortos, perfectos para URLs: /pais/arg en vez de /pais/argentina. Es la mejor práctica: URLs cortas, estables (no cambian si el nombre del país cambia), y compartibles. Si usas el nombre del país, una coma o un acento puede romper la URL. Los códigos cca3 son la forma estándar ISO y los usan la mayoría de las APIs geográficas.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

3.4.5 Rutas protegidas: lógica de autenticación

Las rutas protegidas son páginas que solo usuarios autenticados pueden ver: /dashboard, /perfil, /admin. Se implementan con un componente envoltorio (Higher Order Component) que verifica si el usuario está logueado, y si no, lo redirige a /login. Es el patrón estándar para apps con auth, y se complementa con tu backend de JWT del Tema 2.10.

import { Navigate, useLocation } from 'react-router-dom';

// Componente que protege rutas: si no hay token, redirige a /login
function RutaProtegida({ children, requiereAdmin = false }) {
  const token = localStorage.getItem('token');
  const usuario = JSON.parse(localStorage.getItem('usuario') || 'null');
  const location = useLocation();

  if (!token) {
    // Redirige a login, pero guarda DÓNDE quería ir el usuario para volver después
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiereAdmin && usuario?.rol !== 'admin') {
    return <Navigate to="/" replace />;  // no es admin: a home
  }

  return children;  // todo OK, renderiza la ruta
}

// Uso en App.jsx:
<Routes>
  <Route path="/" element={<Inicio />} />
  <Route path="/login" element={<Login />} />
  <Route path="/pais/:id" element={<DetallePais />} />
  
  {/* Rutas protegidas */}
  <Route path="/perfil" element={
    <RutaProtegida>
      <Perfil />
    </RutaProtegida>
  } />
  
  <Route path="/dashboard" element={
    <RutaProtegida>
      <Dashboard />
    </RutaProtegida>
  } />
  
  <Route path="/admin" element={
    <RutaProtegida requiereAdmin>
      <AdminPanel />
    </RutaProtegida>
  } />
  
  <Route path="*" element={<NoEncontrada />} />
</Routes>

// En Login, después de éxito: redirige a la página que quería ir
function Login() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from?.pathname || '/';

  async function handleLogin() {
    // ... login ...
    navigate(from, { replace: true });  // vuelve a donde quería ir
  }
}

RutaProtegida: el componente que verifica autenticación

⭐ Protección en frontend = UX, en backend = seguridad: La protección SOLO en el frontend NO es seguridad real. Un usuario puede manipular el DOM, borrar el localStorage, y acceder a las rutas igual. La VERDADERA seguridad está en el backend: el frontend oculta la UI, pero el backend SIEMPRE valida el JWT y los permisos en cada petición. Es defensa en profundidad: el frontend da UX (ocultar lo que no puedes usar), el backend da seguridad (rechazar lo que no deberías poder hacer). Si solo tienes frontend, cualquier usuario avanzado entra. Si solo tienes backend, la UI muestra cosas que el usuario no puede usar. Ambos = seguro + buena UX.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

3.4.6 Proyecto real: app de países con páginas de detalle

Proyecto integrador: tomaremos la app de países del Tema 3.2 y le agregaremos React Router. La lista será la home (/), cada país tendrá su página de detalle (/pais/:id), habrá una página 'Acerca de' (/sobre) y un 404 elegante. Es el patrón fundamental del 90% de las SPAs reales: lista + detalle + páginas estáticas.

Estructura de carpetas del proyecto

paises-app-con-router/
├── src/
│   ├── App.jsx                    # Router principal
│   ├── components/
│   │   ├── Navbar.jsx              # navegación
│   │   ├── PaisCard.jsx             # tarjeta de país (lista)
│   │   ├── Buscador.jsx            # input de búsqueda
│   │   ├── FiltroRegion.jsx        # filtro por región
│   │   └── ThemeToggle.jsx         # botón tema claro/oscuro
│   ├── pages/
│   │   ├── Inicio.jsx               # lista de países (home)
│   │   ├── DetallePais.jsx          # /pais/:id
│   │   ├── Sobre.jsx                 # /sobre (estática)
│   │   └── NoEncontrada.jsx         # 404
│   ├── hooks/
│   │   ├── useFetch.js              # custom hook para fetch
│   │   └── usePaisesFiltrados.js    # custom hook para filtrar
│   └── styles/
│       ├── tokens.css               # design system
│       └── *.module.css             # CSS Modules por componente

Estructura de carpetas profesional con React Router

App.jsx con todas las rutas

import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
import { Inicio } from './pages/Inicio';
import { DetallePais } from './pages/DetallePais';
import { Sobre } from './pages/Sobre';
import { NoEncontrada } from './pages/NoEncontrada';
import { Navbar } from './components/Navbar';
import './styles/tokens.css';

function App() {
  return (
    <BrowserRouter>
      <Navbar />
      <main className="container">
        <Routes>
          <Route path="/" element={<Inicio />} />
          <Route path="/pais/:id" element={<DetallePais />} />
          <Route path="/sobre" element={<Sobre />} />
          <Route path="*" element={<NoEncontrada />} />
        </Routes>
      </main>
      <footer>
        <p>Construido con React, Vite y REST Countries API</p>
      </footer>
    </BrowserRouter>
  );
}

function Navbar() {
  return (
    <nav className="navbar">
      <NavLink to="/" className={({isActive}) => isActive ? 'active' : ''}>🌍 Países</NavLink>
      <NavLink to="/sobre" className={({isActive}) => isActive ? 'active' : ''}>Sobre</NavLink>
    </nav>
  );
}

App.jsx con React Router v6

⭐ Lista + Detalle + Estática + 404: el patrón del 90%: El proyecto integrador de este tema es el patrón del 90% de las SPAs reales: home con lista, página de detalle con parámetro dinámico, página estática (Sobre), y 404. Una vez que dominas este patrón, lo aplicas a CUALQUIER dominio: e-commerce (lista de productos + /producto/:id), blog (lista de posts + /post/:slug), red social (feed + /user/:username), dashboard (lista de items + /item/:id). Es el esqueleto fundamental. Combínalo con lo que aprendiste: useFetch para cargar datos, useState para estado, CSS Modules o Tailwind para estilos. Es la base de tu portafolio profesional.
🎬
Video del instructor

El administrador aún no ha insertado un video para esta sección.

📚 Contenido ampliado

Material adicional, referencias externas verificadas y ejemplos extendidos.

🤖 AI Mission

React Router es lo que convierte un componente suelto en una aplicación.

Misión: Toma la app de países del Tema 3.2 y conviértela en una SPA multi-página: lista como home, página de detalle con info extendida del país, página 'Acerca de', 404 elegante, tema oscuro, y rutas protegidas. Pídele a la IA que revise tu estructura. Anota: ¿qué fue más difícil: rutas dinámicas, rutas protegidas, o la navegación general?

Pasos sugeridos

  1. Instala: npm install react-router-dom.
  2. Crea pages/Inicio.jsx: tu lista de países del Tema 3.2.
  3. Crea pages/DetallePais.jsx: useParams para leer el :id, fetch a /alpha/{id} para info extendida, muestra capital, población, región, idiomas, monedas, países fronterizos.
  4. Crea pages/Sobre.jsx: estática con info del proyecto.
  5. Crea pages/NoEncontrada.jsx: 404 con diseño amigable (no '404 error' triste).
  6. Modifica PaisCard.jsx: envuelve en <Link to={`/pais/${pais.cca3}`}>.
  7. Crea components/Navbar.jsx: usa NavLink con clase active.
  8. App.jsx: BrowserRouter + Routes con las 4 rutas + 404.
  9. Crea RutaProtegida: lógica de auth (puedes usar localStorage fake).
  10. Crea pages/Perfil.jsx (protegida) y agrégala a las rutas.
  11. Prueba: click en un país → /pais/arg → ver detalles → click en país fronterizo → /pais/bra → ver Brasil. Todo sin recargar.
  12. Pídele a la IA: 'Tengo esta app con React Router. Sugiere 2 mejoras: accesibilidad (skip links, focus management) o UX (breadcrumbs, scroll restoration). NO me des código, dime QUÉ agregar.'
  13. Aplica 2 mejoras.
  14. Anota: 3 cosas que aprendiste con React Router que ningún tutorial te enseñó.

📓 Entregable: URL pública de la app con routing, capturas de: home, detalle de país, países fronterizos enlazados, 404, ruta protegida, y media página de cuaderno con tu reflexión.

🚫 Errores típicos de razonamiento

Error 1: Usar <a href="/ruta"> en lugar de <Link to="/ruta"> en una SPA.
Por qué: <a href> causa una RECARGA COMPLETA de la página: pierdes todo el estado, los datos cargados, la posición de scroll, y das una experiencia de página web estática. <Link to> de React Router navega sin recargar, manteniendo el estado y la experiencia de SPA. Es uno de los errores más comunes de juniors que vienen de HTML clásico. La regla: <a> para ir a OTRO sitio web (https://google.com), <Link> para navegar DENTRO de tu SPA.
Error 2: Olvidar BrowserRouter o ponerlo dentro de Routes.
Por qué: BrowserRouter DEBE envolver TODA tu app (típicamente en main.jsx o App.jsx en el top level). Si está DENTRO de Routes, las rutas internas no funcionan porque no tienen el contexto de routing. Si está FUERA pero no envuelve todo, los links y useNavigate fallan. La regla: BrowserRouter va en el componente MÁS EXTERNO (después de StrictMode y providers, pero antes de Routes).
Error 3: Usar Switch (v5) en vez de Routes (v6).
Por qué: React Router v6 cambió Switch por Routes. Si ves tutoriales viejos con <Switch><Route component={X}/></Switch>, es v5. En v6 es <Routes><Route element={<X/>}/></Routes>. Además, v6 ya no usa exact (todas las rutas son exactas por default). Migrar de v5 a v6 es relativamente simple: cambiar Switch por Routes, component por element, y useHistory por useNavigate. Pero empieza con v6 desde el principio para evitar migración.
Error 4: Poner la ruta / antes que las rutas dinámicas.
Por qué: Las Routes se evalúan en ORDEN y la primera que coincide gana. Si pones <Route path="/" element={<Home/>}/> ANTES de <Route path="/pais/:id" element={<Detalle/>}/>, NUNCA llegarás a la ruta del detalle, porque / matchea TODO. Pon las rutas MÁS ESPECÍFICAS primero y la / al final. O usa path="*" para el 404 al final, que solo se activa si NINGUNA ruta coincidió.
Error 5: Creer que las rutas protegidas del frontend son 'seguridad real'.
Por qué: La protección en el frontend es solo UX: oculta lo que el usuario no puede usar. Pero un usuario avanzado puede manipular el DOM, borrar el localStorage, y acceder a las rutas. La VERDADERA seguridad está en el BACKEND: tu API de Express/Supabase valida el JWT y los permisos en CADA petición, sin importar lo que el frontend muestre. Es defensa en profundidad: frontend para UX, backend para seguridad real. La regla: NUNCA confíes solo en el frontend para auth.

🧪 Laboratorio práctico

Una SPA con routing es una aplicación real. Sin routing, es un componente suelto.

Laboratorio: App de países con React Router v6 + páginas de detalle + 404 elegante

Objetivo: Convertir la app de países del Tema 3.2 en una SPA multi-página con React Router v6: home con lista, detalle con info extendida, página 'Acerca de', 404, Navbar con NavLink, y rutas protegidas (Perfil). Practicarás: BrowserRouter, Routes, Route, Link, NavLink, useParams, useNavigate, Navigate, y rutas protegidas.

Pasos

  1. Crea el proyecto: npm create vite@latest paises-router -- --template react.
  2. Instala: npm install react-router-dom.
  3. Crea pages/Inicio.jsx: la lista de países (mueve el código de la versión anterior).
  4. Crea pages/DetallePais.jsx: useParams para :id, fetch a https://restcountries.com/v3.1/alpha/{id}, muestra info extendida (capital, población, región, idiomas, monedas, países fronterizos con Link).
  5. Crea pages/Sobre.jsx: estática con info del proyecto, usa CSS.
  6. Crea pages/NoEncontrada.jsx: 404 amigable con link de vuelta al home.
  7. Crea pages/Perfil.jsx: contenido protegido con localStorage fake.
  8. Crea components/Navbar.jsx: NavLink con clase active condicional.
  9. Crea components/RutaProtegida.jsx: verifica token en localStorage, redirige a /login si no hay.
  10. App.jsx: BrowserRouter + Routes con todas las rutas + 404.
  11. PaisCard: Link to={`/pais/${pais.cca3}`}.
  12. Prueba: click en país → /pais/arg → ver detalle → click en fronterizo → /pais/bra → ver Brasil. Todo sin recargar.
  13. Prueba el botón 'Atrás' del navegador: debe volver a la lista sin recargar.
  14. Sube a Vercel. Comparte URL.
  15. Haz commit: 'feat: SPA multi-página con React Router v6'.

📓 Entregable: URL pública con routing funcional, capturas de: home, detalle de país con info extendida, países fronterizos enlazados, 404 elegante, ruta protegida, y commit en Git.

📓 Tu cuaderno: Anota pseudocódigo, diagramas, errores que encontraste y respuestas a "explica sin código". La escritura manual refuerza tu razonamiento. Tu profesor puede pedirte que subas fotos de páginas específicas.