React breadcrumbs

React breadcrumbs

I recently needed a simple breadcrumbs implementation in a React app, and thought I’d use https://github.com/NiklasMencke/nextjs-breadcrumbs‘s source code as a starting point for my own implementation. I thought I’d share the source code here in case anyone might benefit from it,.

But first, at little context. In the app’s URL, we might see something like this: http://<url>/stores/3/products/58. Note that the numbers are database ID’s for the stores and products. For my breadcrumbs, I wanted the names instead if database ID’s, so I needed the breadcrumb function to translate store and product ID’s into actual names.

With that in mind, let’s get to the code. Here’s the main business logic:

import React, { useEffect, useState } from 'react';
import { Link, useRouter } from "blitz"
import { Text, HStack } from "@chakra-ui/react"
import { ChevronRightIcon } from "@chakra-ui/icons"
import { getProductById, getStoreById, getTickerById } from "app/core/libs/dbUtils"

type BreadcrumbElement = {
    breadcrumb: string
    href: string
}

export const Breadcrumbs = () => {
    const router = useRouter();
    const initialData: BreadcrumbElement[] = []
    const [breadcrumbs, setBreadcrumbs] = useState(initialData);

    useEffect(() => {
        const generateBreadcrumbs = async () => {
            const pathTokens = router.asPath.split('/');
            pathTokens.shift();

            getBreadcrumElementsByPathTokens(pathTokens).then((breadcrumbElements: BreadcrumbElement[]) => {
                setBreadcrumbs(breadcrumbElements)
            }).catch(error => {
                console.error(`Could not get data: ${JSON.stringify(error)}`)
            })
        }

        if (router.isReady) {
            generateBreadcrumbs()
        }
    }, [router]);

    if (!breadcrumbs) {
        return null;
    }

    return (
        <nav aria-label="breadcrumbs">
            <HStack>
                <Text key="home" fontStyle="italic" fontSize="xs">
                    <a href="/">HOME</a>
                </Text>
                {breadcrumbs.map((breadcrumbElement: BreadcrumbElement, index: number) => {
                    const Separator = () => {
                        if (index < breadcrumbs.length) {
                            return <ChevronRightIcon />
                        }
                        else return null
                    }

                    if (breadcrumbElement.breadcrumb === undefined) return <>(not found)</>

                    return (
                        <HStack key={index} >
                            <Separator />
                            <Text key={index} fontStyle="italic" fontSize="xs">
                                <Link href={breadcrumbElement.href}>
                                    <a>
                                        {convertBreadcrumb(breadcrumbElement.breadcrumb)}
                                    </a>
                                </Link>
                            </Text>
                        </HStack>
                    );
                })}
            </HStack>
        </nav>
    );
};

And here’s the library functions referenced in the above code:

const convertBreadcrumb = (string: string) => {
    if (!string) return "N/A"
    return string
        .replace(/-/g, ' ')
        .replace(/oe/g, 'ö')
        .replace(/ae/g, 'ä')
        .replace(/ue/g, 'ü')
        .toUpperCase();
};

// https://javascript.plainenglish.io/how-to-use-async-function-in-react-hook-useeffect-typescript-js-6204a788a435
const generateBreadcrumbEntryByPath = async (pathElement: string, previousPathElement): Promise<string> => {
    if (previousPathElement === "products") {
        return await getProductById(Number(pathElement))
    }

    else if (previousPathElement === "stores") {
        const store = await getStoreById(Number(pathElement))
        return store.name
    }

    else return pathElement
}

const getBreadcrumbElement = async (pathTokens: string[], inputPathToken: string, index: number): Promise<BreadcrumbElement> => {

    const generateHref = (linkPath: string[], i: number) => {
        return '/' + linkPath.slice(0, i + 1).join('/')
    }

    let newPathElement = inputPathToken
    if (index > 0) {
        const previousBreadcrumb = pathTokens[index - 1]

        try {
            newPathElement = await generateBreadcrumbEntryByPath(inputPathToken, previousBreadcrumb)
        }
        catch (error) {
            console.error(`Error from generateBreadcrumbEntryByPath: ${JSON.stringify(error)}`)
            const breadcrumbElement: BreadcrumbElement = { breadcrumb: "", href: "" };
            return breadcrumbElement
        }
    }

    const breadcrumbElement: BreadcrumbElement = { breadcrumb: newPathElement, href: generateHref(pathTokens, index) };
    return breadcrumbElement
}

const getBreadcrumElementsByPathTokens = async (pathTokens: string[]): Promise<BreadcrumbElement[]> => {
    return Promise.all(pathTokens.map(async (pathToken, index) => {
        const breadcrumbElement: BreadcrumbElement = await getBreadcrumbElement(pathTokens, pathToken, index)
        return breadcrumbElement
    }))
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: