Link y hooks useNavigate useLocation de React Router
Introducción
En esta clase vamos a modernizar por completo el enrutado de nuestra aplicación:
- Dejamos atrás implementaciones manuales de navegación.
- Mantenemos nuestro componente
Linkpropio, pero cambiamos su interior. - Integramos los hooks nativos de React Router:
useNavigateyuseLocation. - Aplicamos un abstraction pattern, para que React Router esté encapsulado y podamos cambiarlo sin tocar toda la app.
El objetivo es mejorar nuestra arquitectura manteniendo la misma API externa que ya tenemos funcionando.
1. Eliminando nuestro código manual del Link
Hasta ahora teníamos un componente Link propio que hacía cosas a mano:
- Escuchar el click
- Prevenir el default
- Usar
pushState - Notificar a nuestro sistema de routing
Pero React Router ya trae su propio Link, así que ese comportamiento manual ya no lo necesitamos.
El Link que teníamos antes
export function Link({ to, children, ...props }) {
const handleClick = (e) => {
e.preventDefault()
window.history.pushState({}, '', to)
// Disparar evento custom para notificar cambios...
}
return (
<a href={to} onClick={handleClick} {...props}>
{children}
</a>
)
}
Todo esto manualmente… todo esto no lo necesitamos ya porque React Router ya tiene un componente Link.
Lo que hacemos ahora
Mantenemos nuestro Link, pero cambiamos su interior:
import { Link as RRLink } from 'react-router'
export function Link(props) {
return <RRLink {...props} />
}
Así:
- Toda la app sigue usando nuestro
<Link>. - Si mañana queremos usar otra librería de enrutado, solo cambiamos este archivo único.
- No tenemos que revisar todos los componentes uno por uno.
En lugar de volver a utilizar el componente Link en todos los sitios, lo estamos cambiando en un solo sitio.
2. ¿Por qué hacemos esto? Abstraction pattern
Esto es una técnica que se llamaría abstraction pattern o dependency abstraction pattern.
Ventajas de este patrón
- No acoplamos la aplicación a React Router: Nuestros componentes no saben que existe React Router.
- Si cambiamos de dependencia, solo tocamos nuestra capa de abstracción: Un único archivo en lugar de decenas.
- Todo el código que antes importaba
react-routerahora sigue funcionando sin cambios: La API pública no cambia.
El principio detrás
Cuando trabajas con dependencias externas (librerías de terceros), es buena práctica crear una capa intermedia que:
- Exponga una API propia y estable
- Oculte los detalles de implementación de la librería
- Centralice todos los imports de esa librería en un solo lugar
Si la librería cambia, actualizas, o migras, solo modificas esa capa de abstracción.
3. Sustituir navigateTo por useNavigate
Antes teníamos un router propio con un método navigateTo que hacía la navegación manualmente.
React Router ofrece su propio hook: useNavigate.
El problema con nuestra implementación manual
function useRouter() {
const navigateTo = (path) => {
window.history.pushState({}, '', path)
// Disparar eventos custom
// Actualizar estado
// ... un montón de código manual
}
return { navigateTo }
}
Esto funciona, pero:
- Tenemos que mantenerlo nosotros
- Puede tener bugs
- Reinventamos la rueda
La solución con useNavigate
React Router tiene su propio hook que se llama useNavigate. Vamos a usarlo:
import { useNavigate } from 'react-router'
export function useRouter() {
const navigate = useNavigate()
const navigateTo = (path) => {
navigate(path)
}
return { navigateTo }
}
¿Qué ganamos?
- No necesitamos implementar a mano el pushState: React Router lo hace mejor.
- Los componentes siguen llamando a
navigateTo: La API externa no cambia. - Hemos cambiado la dependencia sin romper nada: Todo sigue funcionando.
4. Eliminando el cálculo manual del currentPath
Antes teníamos:
- Un estado
currentPath - Un
useEffectescuchando eventos del history - Una lógica propia para detectar cambios de URL
React Router incluye useLocation, que nos da:
pathname- la ruta actualsearch- los query paramshash- el hash de la URLstate- estado pasado en la navegación
El código manual que teníamos
function useRouter() {
const [currentPath, setCurrentPath] = useState(window.location.pathname)
useEffect(() => {
const handleLocationChange = () => {
setCurrentPath(window.location.pathname)
}
window.addEventListener('popstate', handleLocationChange)
window.addEventListener('pushstate', handleLocationChange)
return () => {
window.removeEventListener('popstate', handleLocationChange)
window.removeEventListener('pushstate', handleLocationChange)
}
}, [])
return { currentPath }
}
Todo este useEffect y todo esto ya no es necesario.
La solución con useLocation
El currentPath simplemente es el location.pathname:
import { useLocation } from 'react-router'
export function useRouter() {
const location = useLocation()
const currentPath = location.pathname
return {
currentPath,
location,
}
}
Mucho más limpio y sin efectos secundarios que manejar.
5. El hook useRouter completo
Juntando todo lo anterior, nuestro hook useRouter queda así:
import { useNavigate, useLocation } from 'react-router'
export function useRouter() {
const navigate = useNavigate()
const location = useLocation()
function navigateTo(path) {
navigate(path)
}
const currentPath = location.pathname
return {
currentPath,
navigateTo,
}
}
¿Qué hemos conseguido?
- Eliminamos todo el código manual: Nada de
pushState, nada de eventos custom, nada deuseEffect. - React Router hace el trabajo pesado: Y lo hace mejor que nosotros.
- Nuestra API sigue igual: Los componentes que usaban
useRouterno cambian.
6. Beneficio clave: todo sigue funcionando igual
Todo está funcionando correctamente utilizando el nuevo router… pero solo cambiando un sitio.
Los componentes de tu aplicación siguen usando:
import { useRouter } from '../hooks/useRouter'
export function SomeComponent() {
const { currentPath, navigateTo } = useRouter()
return (
<div>
<p>Estás en: {currentPath}</p>
<button onClick={() => navigateTo('/home')}>Ir a Home</button>
</div>
)
}
Y siguen usando:
import { Link } from '../components/Link'
export function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
)
}
Ninguno de estos componentes sabe que existe React Router. Solo saben que pueden usar Link, useRouter, navigateTo y currentPath.
Por qué esto es tan poderoso
Hemos quitado toda la información que teníamos antes de forma manual.
Esto es lo bueno de haber separado algunas cosas en custom hooks.
En lugar de ir cambiando componente por componente, lo cambiamos en un único sitio.
Si mañana aparece otra librería de routing que queremos usar:
- Cambiamos la implementación de
Link(1 archivo) - Cambiamos la implementación de
useRouter(1 archivo) - Todo lo demás sigue funcionando sin tocar una línea de código
7. Comparación antes y después
Antes (implementación manual)
// Link.jsx - Implementación manual
export function Link({ to, children, ...props }) {
const handleClick = (e) => {
e.preventDefault()
window.history.pushState({}, '', to)
window.dispatchEvent(new Event('pushstate'))
}
return (
<a href={to} onClick={handleClick} {...props}>
{children}
</a>
)
}
// useRouter.js - Implementación manual
export function useRouter() {
const [currentPath, setCurrentPath] = useState(window.location.pathname)
useEffect(() => {
const handleLocationChange = () => {
setCurrentPath(window.location.pathname)
}
window.addEventListener('popstate', handleLocationChange)
window.addEventListener('pushstate', handleLocationChange)
return () => {
window.removeEventListener('popstate', handleLocationChange)
window.removeEventListener('pushstate', handleLocationChange)
}
}, [])
const navigateTo = (path) => {
window.history.pushState({}, '', path)
window.dispatchEvent(new Event('pushstate'))
}
return {
currentPath,
navigateTo,
}
}
Problemas:
- Mucho código manual
- Propenso a bugs
- Difícil de mantener
- Reinventamos la rueda
Después (con React Router)
// Link.jsx - Usando React Router
import { Link as RRLink } from 'react-router'
export function Link(props) {
return <RRLink {...props} />
}
// useRouter.js - Usando React Router
import { useNavigate, useLocation } from 'react-router'
export function useRouter() {
const navigate = useNavigate()
const location = useLocation()
function navigateTo(path) {
navigate(path)
}
const currentPath = location.pathname
return {
currentPath,
navigateTo,
}
}
Ventajas:
- Código mínimo
- Usa una librería probada y mantenida
- Más robusto y confiable
- Fácil de mantener
8. Extensiones del patrón
Una vez que tienes esta capa de abstracción, puedes extenderla fácilmente:
Añadir funcionalidad extra
export function useRouter() {
const navigate = useNavigate()
const location = useLocation()
function navigateTo(path) {
// Aquí puedes añadir analytics, logging, etc.
console.log('Navegando a:', path)
navigate(path)
}
function goBack() {
navigate(-1)
}
const currentPath = location.pathname
return {
currentPath,
navigateTo,
goBack,
location,
}
}
Todos los componentes se benefician automáticamente
No necesitas cambiar nada en los componentes. La nueva funcionalidad está disponible inmediatamente:
const { goBack } = useRouter()
<button onClick={goBack}>← Volver</button>
Resumen
En esta clase hemos hecho:
- Mantener nuestro componente
Linkpero cambiar su implementación interna para usar elLinkdereact-router. - Reemplazar nuestra navegación manual por
useNavigate. - Reemplazar nuestro cálculo manual de la ruta por
useLocation. - Aplicar un abstraction pattern para encapsular la dependencia.
- Aprovechar que toda la lógica está centralizada en
useRoutery no en decenas de componentes. - Conseguir que todo funcione igual, pero con una arquitectura más limpia y más fácil de modificar.
La clave está en: cambiar la implementación sin cambiar la interfaz. Tus componentes siguen usando la misma API, pero por debajo ahora usas React Router en lugar de código manual.
Este patrón te ahorrará muchísimo tiempo cuando necesites hacer cambios, actualizar dependencias, o incluso migrar a otra solución de routing en el futuro.