paint-brush
Implementar una ruta de navegación dinámica en React/NextJSby@dstarner
25,929
25,929

Implementar una ruta de navegación dinámica en React/NextJS

Dan Starner2022/01/07
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow
ES

Las migas de pan son una herramienta de navegación del sitio web que permite a un usuario ver la "pila" de su página actual de cómo está anidada debajo de las páginas principales. Aprenda a crear un componente Breadcrumbs para Nextjs y MUI que crea dinámica y contextualmente las migas en función de la ruta actual.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Implementar una ruta de navegación dinámica en React/NextJS
Dan Starner HackerNoon profile picture


Las migas de pan son una herramienta de navegación del sitio web que permite a un usuario ver la "pila" de su página actual de cómo está anidada debajo de las páginas principales. Luego, los usuarios pueden regresar a una página principal haciendo clic en el enlace de ruta de navegación asociado. Estas "migajas" aumentan la experiencia del usuario de la aplicación, lo que facilita que los usuarios naveguen por páginas anidadas de manera eficiente y efectiva.

Ejemplo de migas de pan

Las migas de pan son lo suficientemente populares como para que, si está creando un panel de control web o una aplicación, haya considerado agregarlas. Generar estos enlaces de migas de pan de manera eficiente y con el contexto apropiado es clave para una experiencia de usuario mejorada.


Construyamos un componente inteligente NextBreadcrumbs React que analice la ruta actual y construya una pantalla de migas de pan dinámicas que pueda manejar rutas estáticas y dinámicas de manera eficiente.


Mis proyectos generalmente giran en torno a Nextjs y MUI (anteriormente Material-UI), por lo que ese es el ángulo desde el que abordaré este problema, aunque la solución debería funcionar para cualquier aplicación relacionada con Nextjs.

Migas de pan de ruta estática

Para empezar, nuestro componente NextBreadcrumbs solo manejará rutas estáticas, lo que significa que nuestro proyecto solo tiene páginas estáticas definidas en el directorio de pages .

Los siguientes son ejemplos de rutas estáticas porque no contienen [ s y ] s en los nombres de las rutas, lo que significa que la estructura del directorio se alinea 1:1 exactamente con las URL esperadas que sirven.


  • pages/index.js --> /

  • pages/about.js --> /about

  • pages/my/super/nested/route.js --> /my/super/nested/route


La solución se ampliará para manejar rutas dinámicas más adelante.


Definición del componente básico

Podemos comenzar con el componente básico que utiliza el componente Breadcrumbs de MUI como línea de base.


 import Breadcrumbs from '@mui/material/Breadcrumbs'; import * as React from 'react'; export default function NextBreadcrumbs() { return ( <Breadcrumbs aria-label="breadcrumb" /> ); }


Lo anterior crea la estructura básica del componente NextBreadcrumbs React, importa las dependencias correctas y representa un componente MUI de Breadcrumbs vacío.


Luego podemos agregar los next/router , que nos permitirán construir las migas de pan a partir de la ruta actual.


También creamos un componente Crumb que se usará para representar cada enlace. Este es un componente bastante tonto por ahora, excepto que mostrará texto normal en lugar de un enlace para la última ruta de navegación.


En una situación como /settings/notifications , se mostraría de la siguiente manera:


 Home (/ link) > Settings (/settings link) > Notifications (no link)


Esto se debe a que el usuario ya está en la última página de migas de pan, por lo que no es necesario vincular a la misma página. Todas las demás migajas se representan como enlaces en los que se puede hacer clic.


 import Breadcrumbs from '@mui/material/Breadcrumbs'; import Link from '@mui/material/Link'; import Typography from '@mui/material/Typography'; import { useRouter } from 'next/router'; import React from 'react'; export default function NextBreadcrumbs() { // Gives us ability to load the current route details const router = useRouter(); return ( <Breadcrumbs aria-label="breadcrumb" /> ); } // Each individual "crumb" in the breadcrumbs list function Crumb({ text, href, last=false }) { // The last crumb is rendered as normal text since we are already on the page if (last) { return <Typography color="text.primary">{text}</Typography> } // All other crumbs will be rendered as links that can be visited return ( <Link underline="hover" color="inherit" href={href}> {text} </Link> ); }


Con este diseño, podemos volver a sumergirnos en el componente NextBreadcrumbs para generar las migas de pan de la ruta. Parte del código existente comenzará a omitirse para mantener las piezas de código más pequeñas. El ejemplo completo se muestra a continuación.


Generaremos una lista de objetos de migas de pan que contienen la información que cada elemento de Crumb debe representar. Cada ruta de navegación se creará analizando la propiedad asPath del enrutador asPath , que es una cadena que contiene la ruta como se muestra en la barra de URL del navegador.

Quitaremos todos los parámetros de consulta, como ?query=value , de la URL para que el proceso de creación de la ruta de navegación sea más sencillo.


 export default function NextBreadcrumbs() { // Gives us ability to load the current route details const router = useRouter(); function generateBreadcrumbs() { // Remove any query parameters, as those aren't included in breadcrumbs const asPathWithoutQuery = router.asPath.split("?")[0]; // Break down the path between "/"s, removing empty entities // Ex:"/my/nested/path" --> ["my", "nested", "path"] const asPathNestedRoutes = asPathWithoutQuery.split("/") .filter(v => v.length > 0); // Iterate over the list of nested route parts and build // a "crumb" object for each one. const crumblist = asPathParts.map((subpath, idx) => { // We can get the partial nested route for the crumb // by joining together the path parts up to this point. const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/"); // The title will just be the route string for now const title = subpath; return { href, text }; }) // Add in a default "Home" crumb for the top-level return [{ href: "/", text: "Home" }, ...crumblist]; } // Call the function to generate the breadcrumbs list const breadcrumbs = generateBreadcrumbs(); return ( <Breadcrumbs aria-label="breadcrumb" /> ); }


Con esta lista de migas de pan, ahora podemos renderizarlas usando los componentes Breadcrumbs y Crumb . Como se mencionó anteriormente, solo se muestra la parte de return de nuestro componente por razones de brevedad.


 // ...rest of NextBreadcrumbs component above... return ( {/* The old breadcrumb ending with '/>' was converted into this */} <Breadcrumbs aria-label="breadcrumb"> {/* Iterate through the crumbs, and render each individually. We "mark" the last crumb to not have a link. */} {breadcrumbs.map((crumb, idx) => ( <Crumb {...crumb} key={idx} last={idx === breadcrumbs.length - 1} /> ))} </Breadcrumbs> );


Esto debería comenzar a generar algunas migas de pan muy básicas, pero que funcionan, en nuestro sitio una vez renderizado; /user/settings/notifications se representaría como


 Home > user > settings > notifications


Memorización de migas de pan generadas

Sin embargo, hay una mejora rápida que podemos hacer antes de seguir adelante. En este momento, la lista de migas de pan se recrea cada vez que el componente se vuelve a renderizar, por lo que podemos memorizar la lista de migas de una ruta determinada para ahorrar algo de rendimiento. Para lograr esto, podemos envolver nuestra llamada a la función generateBreadcrumbs en el gancho useMemo React.


 const router = useRouter(); // this is the same "generateBreadcrumbs" function, but placed // inside a "useMemo" call that is dependent on "router.asPath" const breadcrumbs = React.useMemo(function generateBreadcrumbs() { const asPathWithoutQuery = router.asPath.split("?")[0]; const asPathNestedRoutes = asPathWithoutQuery.split("/") .filter(v => v.length > 0); const crumblist = asPathParts.map((subpath, idx) => { const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/"); return { href, text: subpath }; }) return [{ href: "/", text: "Home" }, ...crumblist]; }, [router.asPath]); return // ...rest below...


Mejora de la visualización del texto de la ruta de navegación

Antes de comenzar a incorporar rutas dinámicas, podemos limpiar más esta solución actual al incluir una forma agradable de cambiar el texto que se muestra para cada migaja generada.


En este momento, si tenemos una ruta como /user/settings/notifications , se mostrará:


 Home > user > settings > notifications


que no es muy atractivo. Podemos proporcionar una función al componente NextBreadcrumbs que intentará generar un nombre más fácil de usar para cada uno de estos migas de ruta anidadas.


 const _defaultGetDefaultTextGenerator= path => path export default function NextBreadcrumbs({ getDefaultTextGenerator=_defaultGetDefaultTextGenerator }) { const router = useRouter(); // Two things of importance: // 1. The addition of getDefaultTextGenerator in the useMemo dependency list // 2. getDefaultTextGenerator is now being used for building the text property const breadcrumbs = React.useMemo(function generateBreadcrumbs() { const asPathWithoutQuery = router.asPath.split("?")[0]; const asPathNestedRoutes = asPathWithoutQuery.split("/") .filter(v => v.length > 0); const crumblist = asPathParts.map((subpath, idx) => { const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/"); return { href, text: getDefaultTextGenerator(subpath, href) }; }) return [{ href: "/", text: "Home" }, ...crumblist]; }, [router.asPath, getDefaultTextGenerator]); return ( // ...rest below


y luego nuestro componente principal puede tener algo como lo siguiente, para titular las rutas secundarias, o tal vez incluso reemplazarlas con una nueva cadena.


 {/* Assume that `titleize` is written and works appropriately */} <NextBreadcrumbs getDefaultTextGenerator={path => titleize(path)} />


Esta implementación daría como resultado las siguientes migas de pan. El ejemplo de código completo en la parte inferior tiene más ejemplos de esto.


 Home > User > Settings > Notifications


Rutas dinámicas de Nextjs

El enrutador de Nextjs permite incluir rutas dinámicas que usan Pattern Matching para permitir que las URL tengan slugs, UUID y otros valores dinámicos que luego se pasarán a sus vistas.

Por ejemplo, si su aplicación Nextjs tiene un componente de página en pages/post/[post_id].js , entonces las rutas /post/1 y /post/abc coincidirán.


Para nuestro componente de migas de pan, nos gustaría mostrar el nombre de la publicación asociada en lugar de solo su UUID. Esto significa que el componente deberá buscar dinámicamente los datos de la publicación en función de la ruta de la ruta URL anidada y regenerar el texto de la migaja asociada.


En este momento, si visita /post/abc , verá migas de pan que se ven como


 post > abc


pero si la publicación con UUID tiene el título My First Post , entonces queremos cambiar las migas de pan para decir


 post > My First Post


Profundicemos en cómo puede suceder eso usando funciones async .


Enrutador Nextjs: asPath vs nombre de pathname

La instancia de enrutador next/router en nuestro código tiene dos propiedades útiles para nuestro componente NextBreadcrumbs ; asPath y pathname de acceso. El enrutador asPath es la ruta URL como se muestra directamente en la barra de URL del navegador. El nombre de pathname es una versión más interna de la URL que tiene las partes dinámicas de la ruta reemplazadas con sus componentes [parameter] .


Por ejemplo, considere la ruta /post/abc de arriba.


  • El asPath sería /post/abc como se muestra en la URL

  • El nombre de la pathname sería /post/[post_id] como dicta nuestro directorio de pages


Podemos usar estas dos variantes de ruta de URL para crear una forma de obtener información dinámicamente sobre la ruta de navegación, de modo que podamos mostrar al usuario información más adecuada al contexto.


Están sucediendo muchas cosas a continuación, así que vuelva a leerlo, y las notas útiles a continuación, unas cuantas veces si es necesario.


 const _defaultGetTextGenerator = (param, query) => null; const _defaultGetDefaultTextGenerator = path => path; // Pulled out the path part breakdown because its // going to be used by both `asPath` and `pathname` const generatePathParts = pathStr => { const pathWithoutQuery = pathStr.split("?")[0]; return pathWithoutQuery.split("/") .filter(v => v.length > 0); } export default function NextBreadcrumbs({ getTextGenerator=_defaultGetTextGenerator, getDefaultTextGenerator=_defaultGetDefaultTextGenerator }) { const router = useRouter(); const breadcrumbs = React.useMemo(function generateBreadcrumbs() { const asPathNestedRoutes = generatePathParts(router.asPath); const pathnameNestedRoutes = generatePathParts(router.pathname); const crumblist = asPathParts.map((subpath, idx) => { // Pull out and convert "[post_id]" into "post_id" const param = pathnameNestedRoutes[idx].replace("[", "").replace("]", ""); const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/"); return { href, textGenerator: getTextGenerator(param, router.query), text: getDefaultTextGenerator(subpath, href) }; }) return [{ href: "/", text: "Home" }, ...crumblist]; }, [router.asPath, router.pathname, router.query, getTextGenerator, getDefaultTextGenerator]); return ( // ...rest below


  • El desglose de asPath se movió a una función generatePathParts ya que se usa la misma lógica para router.asPath y router.pathname .

  • Determine el parámetro que se alinea con el valor de la ruta dinámica, de modo que abc post_id param

  • El parámetro de ruta anidado y todos los valores de consulta asociados ( param ) se pasan a un getTextGenerator proporcionado que devolverá un valor null o una respuesta Promise que debería devolver la cadena dinámica para usar en la ruta de router.query asociada.

  • La matriz de dependencias useMemo tiene más dependencias agregadas; router.pathname , router.query y getTextGenerator .


Finalmente, necesitamos actualizar el componente Crumb para usar este valor de textGenerator si se proporciona para el objeto Crumb asociado.


 function Crumb({ text: defaultText, textGenerator, href, last=false }) { const [text, setText] = React.useState(defaultText); useEffect(async () => { // If `textGenerator` is nonexistent, then don't do anything if (!Boolean(textGenerator)) { return; } // Run the text generator and set the text again const finalText = await textGenerator(); setText(finalText); }, [textGenerator]); if (last) { return <Typography color="text.primary">{text}</Typography> } return ( <Link underline="hover" color="inherit" href={href}> {text} </Link> ); }


Las migas de pan ahora pueden manejar rutas estáticas y rutas dinámicas de forma limpia con el potencial de mostrar valores fáciles de usar. Si bien el código anterior es la lógica comercial del componente, todo esto se puede usar con un componente principal que se parece al ejemplo final a continuación.

Ejemplo completo


 // NextBreadcrumbs.js const _defaultGetTextGenerator = (param, query) => null; const _defaultGetDefaultTextGenerator = path => path; // Pulled out the path part breakdown because its // going to be used by both `asPath` and `pathname` const generatePathParts = pathStr => { const pathWithoutQuery = pathStr.split("?")[0]; return pathWithoutQuery.split("/") .filter(v => v.length > 0); } export default function NextBreadcrumbs({ getTextGenerator=_defaultGetTextGenerator, getDefaultTextGenerator=_defaultGetDefaultTextGenerator }) { const router = useRouter(); const breadcrumbs = React.useMemo(function generateBreadcrumbs() { const asPathNestedRoutes = generatePathParts(router.asPath); const pathnameNestedRoutes = generatePathParts(router.pathname); const crumblist = asPathParts.map((subpath, idx) => { // Pull out and convert "[post_id]" into "post_id" const param = pathnameNestedRoutes[idx].replace("[", "").replace("]", ""); const href = "/" + asPathNestedRoutes.slice(0, idx + 1).join("/"); return { href, textGenerator: getTextGenerator(param, router.query), text: getDefaultTextGenerator(subpath, href) }; }) return [{ href: "/", text: "Home" }, ...crumblist]; }, [router.asPath, router.pathname, router.query, getTextGenerator, getDefaultTextGenerator]); return ( <Breadcrumbs aria-label="breadcrumb"> {breadcrumbs.map((crumb, idx) => ( <Crumb {...crumb} key={idx} last={idx === breadcrumbs.length - 1} /> ))} </Breadcrumbs> ); } function Crumb({ text: defaultText, textGenerator, href, last=false }) { const [text, setText] = React.useState(defaultText); useEffect(async () => { // If `textGenerator` is nonexistent, then don't do anything if (!Boolean(textGenerator)) { return; } // Run the text generator and set the text again const finalText = await textGenerator(); setText(finalText); }, [textGenerator]); if (last) { return <Typography color="text.primary">{text}</Typography> } return ( <Link underline="hover" color="inherit" href={href}> {text} </Link> ); }


y luego se puede ver un ejemplo del uso de NextBreadcrumbs a continuación. Tenga en cuenta que useCallback se usa para crear solo una referencia a cada función auxiliar, lo que evitará que se vuelvan a procesar innecesariamente las migas de pan cuando/si el componente de diseño de página se vuelve a procesar. También puede mover esto al alcance de nivel superior del archivo, pero no me gusta contaminar el alcance global de esa manera.


 // MyPage.js (Parent Component) import React from 'react'; import NextBreadcrumbs from "./NextBreadcrumbs"; function MyPageLayout() { // Either lookup a nice label for the subpath, or just titleize it const getDefaultTextGenerator = React.useCallback((subpath) => { return { "post": "Posts", "settings": "User Settings", }[subpath] || titleize(subpath); }, []) // Assuming `fetchAPI` loads data from the API and this will use the // parameter name to determine how to resolve the text. In the example, // we fetch the post from the API and return it's `title` property const getTextGenerator = React.useCallback((param, query) => { return { "post_id": () => await fetchAPI(`/posts/${query.post_id}/`).title, }[param]; }, []); return () { <div> {/* ...Whatever else... */} <NextBreadcrumbs getDefaultTextGenerator={getDefaultTextGenerator} getTextGenerator={getTextGenerator} /> {/* ...Whatever else... */} </div> } }

Esta es una de mis publicaciones más profundas y técnicas, así que espero que la hayas disfrutado, y por favor comenta o comunícate para que pueda garantizar la coherencia y la corrección. Con suerte, esta publicación le enseñó algunas estrategias o conceptos sobre Nextjs.


Si te gustó esta o mis otras publicaciones , ¡suscríbete a mi nuevo boletín para recibir actualizaciones tecnológicas semanales!


Publicado por primera vez aquí