28
loading...
This website collects cookies to deliver better user experience
NextBreadcrumbs
React component that will parse the current route and create a dynamic breadcrumbs display that can handle both static & dynamic routes efficiently.NextBreadcrumbs
component will only handle static routes, meaning that our project has only static pages defined in the pages
directory.['s and
] 's in the route names, meaning the directory structure lines up 1:1 precisely with the expected URLs that they serve.pages/index.js
--> /
pages/about.js
--> /about
pages/my/super/nested/route.js
--> /my/super/nested/route
Breadcrumbs
component as a baseline.import Breadcrumbs from '@mui/material/Breadcrumbs';
import * as React from 'react';
export default function NextBreadcrumbs() {
return (
<Breadcrumbs aria-label="breadcrumb" />
);
}
NextBreadcrumbs
React component, imports the correct dependencies, and renders an empty Breadcrumbs
MUI component. next/router
hooks, which will allow us to build the breadcrumbs from the current route.Crumb
component that will be used to render each link. This is a pretty dumb component for now, except that it will render basic text instead of a link for the last breadcrumb./settings/notifications
, it would render as the following:Home (/ link) > Settings (/settings link) > Notifications (no link)
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>
);
}
NextBreadcrumbs
component to generate the breadcrumbs from the route with this layout. Some existing code will start to be omitted to keep the code pieces smaller. The full example is shown below.Crumb
element. Each breadcrumb will be created by parsing the Nextjs router's asPath
property, which is a string containing the route as shown in the browser URL bar. ?query=value
, from the URL to simplify the breadcrumb creation process.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" />
);
}
Breadcrumbs
and Crumb
components. As previously mentioned, only the return
portion of our component is shown for brevity.// ...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>
);
/user/settings/notifications
would render asHome > user > settings > notifications
generateBreadcrumbs
function call in the useMemo
React hook.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...
/user/settings/notifications
, then it will show:Home > user > settings > notifications
NextBreadcrumbs
component to generate a more user-friendly name for each of these nested route crumbs.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
{/* Assume that `titleize` is written and works appropriately */}
<NextBreadcrumbs getDefaultTextGenerator={path => titleize(path)} />
Home > User > Settings > Notifications
pages/post/[post_id].js
, then the routes /post/1
and /post/abc
will match it./post/abc
, you would see breadcrumbs that look likepost > abc
My First Post
, then we want to change the breadcrumbs to saypost > My First Post
async
functions.next/router
router instance in our code has two useful properties for our NextBreadcrumbs
component; asPath
and pathname
. The router asPath
is the URL path as shown directly in the browser's URL bar. The pathname
is a more internal version of the URL that has the dynamic parts of the path replaced with their [parameter]
components./post/abc
from above.asPath
would be /post/abc
as the URL is shownpathname
would be /post/[post_id]
as our pages
directory dictatesconst _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
asPath
breakdown was moved to a generatePathParts
function since the same logic is used for both router.asPath
and router.pathname
.param'eter that lines up with the dynamic route value, so
abcwould result in
post_id`. param'eter and all associated query values (
router.query) are passed to a provided
getTextGenerator which will return either a
null value or a
Promise` response that should return the dynamic string to use in the associated breadcrumb.useMemo
dependency array has more dependencies added; router.pathname
, router.query
, and getTextGenerator
.Crumb
component to use this textGenerator
value if provided for the associated crumb object.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>
);
}
// 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>
);
}
NextBreadcrumbs
being used can be seen below. Note that useCallback
is used to create only one reference to each helper function which will prevent unnecessary re-renders of the breadcrumbs when/if the page layout component re-rendered. Of course, you could move this out to the top-level scope of the file, but I don't like to pollute the global scope like that.// 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>
}
}