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.
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.
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.
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
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...
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
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
.
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.
// 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í