Implementar un router personalizado en Next.js (Parte 1)


Next.js Logo

En este post te voy a enseñar como usualmente manejo las rutas en Next.js (y también en proyectos con sólo React)

Antes que nada, vale la pena mencionar que este enfoque introduce complejidad extra, por lo tanto, no lo uso en todos mis proyectos. En este sitio, por ejemplo, no es realmente necesario ya que consta de algunas pocas páginas. Recomiendo este enfoque para proyectos medios y grandes aunque si tu piensas que tu proyecto no es lo suficientemente grande, te animo a que sigas leyendo ya que esta parte inicial puede aún serte útil, como lo es para mí en casi todos mis proyectos.

El problema

En cada proyecto de frontend tenemos que lidiar con rutas. Manejarlas puede volverse un poco engorroso si no prestamos atención a las rutas en las primeras etapas. Imagina un proyecto con veinte o más rutas y referenciarlas directamente por su nombre como un simple string. Ahora imagina si una de las rutas cambia. ¿Puedes ver el problema? Sí, tenemos que encontrar todos los lugares donde esa ruta es referenciada y actualizarla.

Esto puede llevar a varios problemas. Puedes olvidar actualizar una ruta, la cual redirigirá a una página que no existe. O si estás generando archivos como sitemap.xml o usando cualquier otro tipo de metadatos que requiera el nombre de la ruta, estarían desactualizados.

Solución

Para resolver este problema, vamos a hacer uso del conocido enfoque "Don't repeat yourself" y trasladar esos strings a constantes.

Lo que suelo hacer para empezar es crear una nueva carpeta en mis proyectos llamada config donde coloco un archivo routes.ts. La razón por la que creo una carpeta config es porque normalmente termino usándola también para almacenar otros archivos de configuración más adelante. Pero, si lo prefieres, también puedes simplemente crear un archivo routes.ts individual.

// config/routes.ts
 
export const ROUTES = {
  home: "/",
  aboutUs: "/about-us",
  // Más rutas...
};

Seguidamente, reemplazamos todas las ocurrencias en el código por sus constantes.

Antes

<Link href='/'>Home</Link>
<Link href='/about-us'>About Us</Link>
 
// O las llamadas a los métodos push / relace del router
 
<button type='button' onClick={() => push('/home')}>
  Home
</button>
<button type='button' onClick={() => push('/about-us')}>
  About Us
</button>

Después

import { ROUTES } from '@config/routes';
 
<Link href={ROUTES.home}>Home</Link>
<Link href={ROUTES.aboutUs}>About Us</Link>
 
// O las llamadas a los métodos push / relace del router
 
<button type='button' onClick={() => push(ROUTES.home)}>
  Home
</button>
<button type='button' onClick={() => push(ROUTES.aboutUs)}>
  About Us
</button>

Nota: Siempre defino alias de TypeScript en mis proyectos para simplificar los imports. Ese es el motivo del @ en el import

Por ahora nada demasiado interesante o revolucionario, lo sé. Sólo reemplazamos algunos textos por constantes así que continuemos agregando cosas a esto.

Rutas dinámicas

Probablemente ahora te estés preguntando ¿Qué hacemos con las rutas dinámicas?. ¿Cómo podemos añadirles los parámetros necesarios?

Echemos un vistazo a la siguiente ruta:

https://jsettecase.me/posts/implement-a-custom-router-in-next-js

Como puedes ver, la última parte de la URL es un parámetro dinámico que corresponde al título del post. Si queremos redirigir hacia esa ruta desde dentro del sitio (por ejemplo, desde la lista de publicaciones hacia la publicación en sí), necesitaríamos concatenar manualmente el parámetro dinámico a la ruta de la siguiente manera.

// Posts viniendo desde la base de datos o filesystem
const posts = [
  {
    title: "Implementar un router personalizado en Next.js (Parte 1)",
    url: "implement-a-custom-router-in-next-js",
  },
];
 
posts.map((post) => (
  <Link href={`/posts/${post.url}`}>
    {post.title}
  </Link>
));

Nota: A partir de ahora, voy a usar el componente Link como ejemplo, pero es lo mismo para los métodos push / replace del router

Hasta ahora, con el cambio que acabamos de hacer al mover las rutas de strings a una constantes, aún necesitaríamos hacer lo mismo pero con las constantes en su lugar.

import { ROUTES } from "@config/routes";
 
const posts = [
  {
    title: "Implementar un router personalizado en Next.js (Parte 1)",
    url: "implement-a-custom-router-in-next-js",
  },
];
 
posts.map((post) => (
  <Link href={`${ROUTES.posts}/${post.url}`}>{post.title}</Link>
));

Para algunos de ustedes, este enfoque puede ser suficiente. Sin embargo, tener que concatenar todos los parámetros necesarios cada vez puede resultar un poco molesto. En este caso, solo tenemos un parámetro, pero podríamos tener más de uno. Por esa razón, me gusta implementar la siguiente solución.

En el archivo routes.ts, añade esta función nueva:

// config/routes.ts
 
export type AvailableRoutes = keyof typeof ROUTES
 
export function getRoute(
  routeKey: AvailableRoutes,
  params: Record<string, string> = {}
): string {
  let route = ROUTES[routeKey]
  for (const [k, v] of Object.entries(params)) {
    route = route.replace(`:${k}`, v)
  }
 
  return route
}

El método getRoute toma dos argumentos, el primero debe ser una de las claves definidas en nuestro objeto ROUTES, el segundo es un objeto que contiene los parámetros definidos en la ruta y que quisieramos reemplazar. Estamos usando dos puntos (:) para definir un parámetro dinámico en una ruta, por ejemplo: /posts/:url, pero puedes elegir tu propio formato, sólo tienes que modificar la línea route.replace().

Ahora, podemos definir nuetras dinámicas de la siguiente manera:

// config/routes.ts
 
export const ROUTES = {
  home: "/",
  aboutUs: "/about-us",
  posts: "/posts/:url",
  // Más rutas...
};

Y finalmente, en lugar de importar la constante ROUTES, podemos usar el método getRoute, lo cual simplifica un poco el uso de nuestras rutas y el paso de parámetros a ellas:

import { getRoute } from "@config/routes";
 
const posts = [
  {
    title: "Implementar un router personalizado en Next.js (Parte 1)",
    url: "implement-a-custom-router-in-next-js",
  },
];
 
posts.map((post) => (
  <Link href={getRoute("posts", { url: post.url })}>
    {post.title}
  </Link>
));

Nota: Ten en cuenta que no estamos referenciando rutas mediante strings nuevamente. El primer parámetro del método es la clave en el objeto ROUTES, no el valor, por lo que si en el futuro cambiamos el valor de la ruta, no tenemos que cambiar las llamadas al método. Además, si estás usando TypeScript (y deberías 😉), te impedirá pasar un string incorrecto al método getRoute, ya que tiene un tipado fuerte, y si cambias alguna clave en el objeto ROUTES, TypeScript te indicará todos los lugares donde también debes cambiarla.

Resumen

Esta parte del enfoque es solo un trabajo previo que me gusta realizar para simplificar las cosas antes de agregar el router personalizado. No tener que referenciar rutas mediante strings y permitir que TypeScript nos advierta sobre posibles problemas es una gran ventaja. Creo que puedes aprovechar esto independientemente del tamaño de tu proyecto.

En la segunda parte de esta serie introduciremos el enrutador personalizado y haremos uso del método getRoute. Mantente atento!