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


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)

Esta es la segunda parte de dos, por lo que si no has leído la primera, te recomiendo hacerlo primero.

El problema

En la primera parte de esta serie, pasamos de rutas que eran strings planos a la utilización de una nueva función llamada getRoute, donde podemos pasar el nombre de la ruta tal como está definido en el objeto ROUTES para resolver el camino que queremos. También, manejamos parámetros de ruta a través de esta función, pero todavía tenemos más tareas pendientes para completar esto.

Necesitamos un provider o, en otras palabras, un contexto.

Estoy seguro de que has tenido que lidiar con un CMS u otro tipo de sitio donde necesitas recordar al usuario que guarde los cambios antes de salir de la página. ¿Cuántas veces has construido estas alertas de confirmación con la lógica individualmente en cada página? O tal vez tienes una URL con al menos un parámetro que siempre debe estar presente, como un workspace o un nombre de usuario (por ejemplo, https://demo.com/[nombre-de-empresa o nombre-de-usuario]/perfil). O tal vez tienes alguna lógica específica que necesita ejecutarse en cada cambio de ruta.

Al tener un provider, podemos centralizar y evitar la repetición al manejar todas estas cosas en un solo lugar.

Solución

Primero, comencemos creando una nueva carpeta llamada CustomRouterProvider, donde vamos a colocar solo un index.tsx por ahora. En el archivo, añadamos el siguiente código:

"use client";
 
import { createContext, PropsWithChildren, useContext } from "react";
import { NavigateOptions } from "next/dist/shared/lib/app-router-context";
import { usePathname, useRouter } from "next/navigation";
import { AvailableRoutes, getRoute } from "@/config/routes";
 
type CustomOptions = NavigateOptions & {
  askBeforeLeave?: boolean;
  // Puedes seguir agregando cosas aquí o cambiar esto por completo. Esto es solo un ejemplo
};
 
type CustomPush = (
  route: AvailableRoutes,
  params?: Record<string, string>,
  options?: CustomOptions,
) => void;
 
type CustomReplace = (
  route: AvailableRoutes,
  params?: Record<string, string>,
  options?: CustomOptions,
) => void;
 
type CustomRouterContext = {
  pathname: string;
  push: CustomPush;
  replace: CustomReplace;
};
 
const Context = createContext<CustomRouterContext>({
  pathname: "",
  push: () => undefined,
  replace: () => undefined,
});
 
export function CustomRouterProvider({ children }: PropsWithChildren) {
  const pathname = usePathname();
  const { push: nextPush, replace: nextReplace } = useRouter();
 
  const push: CustomPush = (route, params, options) => {
    if (
      options?.askBeforeLeave &&
      !confirm(
        "Tienes cambios no guardados. ¿Estás seguro de que quieres salir de esta página?",
      )
    ) {
      return;
    }
 
    // ¡Estas funciones y este proveedor pueden personalizarse tanto como quieras!
 
    nextPush(getRoute(route, params), {
      scroll: options?.scroll,
    });
  };
 
  const replace: CustomReplace = (route, params, options) => {
    if (options?.askBeforeLeave) {
      // Implementa la lógica para mostrar una alerta de confirmación.
      // Puedes usar el confirm nativo de JavaScript o implementar uno más bonito.
      // Es tu decisión.
    }
 
    nextReplace(getRoute(route, params), {
      scroll: options?.scroll,
    });
  };
 
  return (
    <Context.Provider
      value={{
        pathname,
        push,
        replace,
      }}
    >
      {children}
    </Context.Provider>
  );
}
 
export function useCustomRouter() {
  return useContext(Context);
}

Nota: Este ejemplo es para Next.js usando el directorio /app, pero puedes hacer lo mismo si estás usando el directorio /pages o incluso React Router en una aplicación de React. Además, me gusta mantener todo lo relacionado con un contexto específico en el mismo archivo, como ves, pero si lo prefieres, puedes dividirlo en varios archivos.

Vamos a profundizar en el código anterior y permíteme omitir los tipos por ahora.

Primero, creamos un contexto de React usando la función createContext disponible en la API de React, y también definimos un valor predeterminado con las propiedades y funciones que estarán disponibles.

Luego, definimos el proviver CustomRouterProvider, que contiene la parte más interesante. Aquí, estamos redefiniendo las funciones nativas push y replace provenientes del router de Next.js para que podamos agregar nuestra propia lógica antes de llamarlas. ¡Esto habilita muchas posibilidades que ahora podemos manejar en un solo lugar!

Finalmente, estamos utilizando el hook useContext disponible también en la API de React y estamos pasando nuestro contexto a él, para que luego podamos usar nuestro propio hook useCustomRouter().

Ahora tenemos que envolver nuestra aplicación con el nuevo provider que acabamos de crear.

Nos va a quedar algo como esto. Recuerda que el nivel en el cual tu elijas añadir el provider va a depender exclusivamente de tu aplicación y tus necesidades.

// app/layout.tsx
export default function RootLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params: Record<string, string>;
}>) {
  return (
    <html lang={params.locale}>
      <body>
        <CustomRouterProvider>
          <Layout>{children}</Layout>
        </CustomRouterProvider>
      </body>
    </html>
  );
}

Nota: En una aplicación React sin Next.js este código podría ir en el archivo index.tsx global que contiene toda tu aplicación.

Finalmente podemos hacer use de nuestro nuevo provider creado anteriormente:

import { useCustomRouter } from "@path/to/CustomRouterProvider";
 
export default MyReactPage() {
  const { push } = useCustomRouter();
 
  return (
    <div>
      <h1>Navegar hacia otra página</h1>
      <a onClick={() => push("aboutUs")}>Acerca de Nosotros</a>
    </div>
  )
}

Nota: Next.js recomienda el use del componente Link ya que ellos hacen una pre carga de las paginas que están linkeadas a la actual. Con este enfoque estamos perdiendo esa capacidad de Next.js pero estamos ganando en simplicidad al momento de manejar lógica centralizada, como alertas globales, etc. Todavía podemos usar el componente Link como vimos en la primera parte de esta guía pero perderíamos las capacidades de nuestro provider. Es algo que tenemos que evaluar de acuerdo a nuestras necesidades.