Rutas protegidas
Algunas partes de una app solo deberían ser visibles para un usuario conectado: un panel de control, una página de cuenta, cualquier cosa que obtenga datos personales. React Router no tiene una característica dedicada de "ruta protegida", y tampoco la necesita. Los componentes de enrutamiento y rutas anidadas se combinan en un patrón: una ruta layout que verifica si el usuario está conectado y renderiza sus hijos o redirige a la página de inicio de sesión.
Una cosa que hay que aclarar antes de cualquier código. Proteger rutas en el cliente es una característica de experiencia del usuario. Mantiene a los visitantes desconectados fuera de páginas que obtendrían sus datos y mostrarían pantallas vacías y rotas. Nunca es seguridad, porque todo en un bundle de cliente es inspectable y evitable.
La verdadera protección sucede en el servidor, que debe rehusar entregar datos a solicitudes que no estén autenticadas. Autenticación cubre cómo funciona esa protección; este capítulo trata sobre la mitad del cliente.
Una ruta layout que requiere autenticación
Una ruta layout es una ruta padre cuyo elemento renderiza UI compartida más un Outlet para sus hijos. Una ruta layout sin ruta no agrega ningún segmento de URL; existe puramente para envolver. Eso la hace el lugar perfecto para una validación de inicio de sesión: envuelve cada ruta que quieras proteger en una ruta layout sin ruta cuyo único trabajo es decidir si el Outlet se renderiza.
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route element={<AuthRequired />}>
<Route path="host" element={<Dashboard />} />
<Route path="host/vans" element={<HostVans />} />
<Route path="host/vans/:id" element={<HostVanDetail />} />
</Route>
</Route>Todo anidado dentro de <Route element={<AuthRequired />}> ahora está protegido, incluyendo rutas con parámetros como host/vans/:id. Proteger una nueva página después es una línea: mueve su ruta dentro del envoltorio.
El guardián tiene dos ramas. Si el usuario está conectado, renderiza el Outlet para que aparezca el hijo que coincide. Si no lo está, renderiza algo que lo envíe a la página de inicio de sesión en su lugar. No renderizar nada es el truco completo: si un componente protegido nunca se renderiza, ninguna obtención de datos dentro de él se inicia.
import { Outlet, Navigate } from 'react-router-dom'
export default function AuthRequired() {
const authenticated = false // marcador de posición para una validación de sesión real
if (!authenticated) {
return <Navigate to="/login" />
}
return <Outlet />
}El curso simula la validación con un booleano, y después con un valor en localStorage, para mantener el enfoque en el enrutamiento. Una validación real lee el estado de la sesión, a menudo entregada a través de context; nuevamente, Autenticación cubre de dónde viene ese estado realmente.
Nota de versión
Los ejemplos aquí importan de react-router-dom, que funciona tanto en v6 como v7; Enrutamiento cubre qué cambió v7 sobre los paquetes. El curso enseña v6 a lo largo de esta sección.
El componente Navigate
Link renderiza un ancla y espera ser cliqueado. Navigate se salta la espera: en cuanto se renderiza, el router mueve al usuario a su ruta to. Eso lo hace la herramienta de redirección para lógica de renderizado.
Tu componente decide, a mitad del renderizado, que el usuario no debería estar aquí, y retorna <Navigate to="/login" /> en lugar de cualquier UI. También hay un hook useNavigate que retorna una función para redirigir desde manejadores de eventos, que es cómo el formulario de inicio de sesión mismo envía al usuario hacia adelante después de un envío exitoso.
Llegar a una página de inicio de sesión desnuda sin explicación es confuso, así que el guardián debería decir por qué el usuario terminó allí. Navigate toma el mismo prop state que Link hace, así que una redirección puede entregar al destino una razón.
if (!authenticated) {
return (
<Navigate
to="/login"
state={{ message: 'Debes iniciar sesión primero' }}
/>
)
}import { useLocation } from 'react-router-dom'
function Login() {
const location = useLocation()
return (
<>
{location.state?.message && <h3>{location.state.message}</h3>}
<h1>Inicia sesión en tu cuenta</h1>
{/* formulario... */}
</>
)
}El encadenamiento opcional importa, porque alguien que hace clic directamente en la página de inicio de sesión nunca pasó por el guardián y encuentra location.state configurado en null.
El historial y reemplazar
El patrón hasta ahora tiene un bug que puedes sentir. Visita una página protegida mientras estás desconectado, obtén una redirección, inicia sesión, luego presiona el botón Atrás del navegador. En lugar de la página en la que estabas antes de todo esto, aterrizas en la página de inicio de sesión nuevamente, completa con su mensaje "debes iniciar sesión primero".
El navegador mantiene un historial: cada navegación empuja una nueva entrada, y Atrás vuelve a la anterior. La redirección empujó /login al historial justo después de la página protegida, y la propia navegación del formulario de inicio de sesión empujó la página protegida encima de eso. Atrás te camina directamente a los restos.
La solución es hacer que las redirecciones reemplacen la entrada actual en lugar de empujar una nueva. En el componente Navigate eso es el prop replace; en la función useNavigate es una opción.
// en AuthRequired: la redirección se reemplaza a sí misma por la página bloqueada
return <Navigate to="/login" state={{ message: 'Debes iniciar sesión primero' }} replace />
// en Login, después de un envío exitoso
navigate('/host', { replace: true })Con ambos en su lugar, el desvío de inicio de sesión nunca sobrevive en el historial. Atrás desde la página protegida ahora va a donde el usuario estaba antes, y la página de inicio de sesión no puede revisitarse por accidente. Una buena regla general: cualquier navegación que el usuario no pidió, que es lo que es una redirección, debería reemplazar.
De vuelta a donde se dirigían
Un rough edge aún permanece. El formulario de inicio de sesión codifica navigate('/host', ...), así que un usuario que intentaba alcanzar /host/vans/2 obtiene tirado a /host después de iniciar sesión. El guardián conoce la URL bloqueada, porque useLocation dentro de AuthRequired describe la página que el usuario intentaba renderizar. Pásalo en el mismo estado de navegación.
export default function AuthRequired() {
const location = useLocation()
const authenticated = false
if (!authenticated) {
return (
<Navigate
to="/login"
state={{
message: 'Debes iniciar sesión primero',
from: location.pathname + location.search + location.hash,
}}
replace
/>
)
}
return <Outlet />
}pathname solo no es la dirección completa: un usuario bloqueado en /host/vans?type=luxury se dirigía a la lista filtrada, y dejar la cadena de consulta quita el filtro. Agregar search y hash reproduce cada parte de la URL que el router rastrea. La página de inicio de sesión lee el valor nuevamente, con un fallback para personas que vinieron a la página de inicio de sesión directamente y por lo tanto no tienen estado.
function Login() {
const location = useLocation()
const navigate = useNavigate()
const from = location.state?.from || '/host'
function handleLogin() {
// ...al completarse:
navigate(from, { replace: true })
}
// ...
}Ahora un enlace compartido a cualquier página protegida sobrevive al desvío de inicio de sesión: bloqueado, redirigido, conectado, y entregado a la URL exacta que querían.
Outlet y las páginas se muestran normalmente. Si no lo está, renderiza Navigate, que lo lleva a la página de inicio de sesión en el momento en que aparece. Agregar replace mantiene la redirección fuera de la memoria del botón Atrás, y pasar a dónde se dirigían permite que la página de inicio de sesión los devuelva allí después.
Recuerda que esto es amabilidad para el usuario; el servidor aún tiene que proteger los datos por sí mismo.
Próximo: Cómo React renderiza, el modelo mental detrás de cada decisión de rendimiento que sigue.

