Next.js Discord

Discord Forum

Best practice pattern: Layout for each page similar, but Breadcrumb content differs

Answered
Ragdoll posted this in #help-forum
Open in Discord
Avatar
RagdollOP
Hi!

I have a design question for my Next.js project regarding the best usage of Next.js features. I wanna create a website where the layout of all routes is structured exactly the same way. Every page needs to have a Breadcrumb as the first HTML element inside the root layout (code provided below). However the content of the Breadcrumb differs because each page has a different route, therefore, the Breadcrumbs should display specific Items for each route/page.

I tried creating a Context for the Breadcrumbs, so every page can change the BreadcrumbItems and the layout can include the BreadcrumbComponent. This method has the huge disadvantage, that all pages need to be Client Components - because every page needs the useEffect hook -, which is unnecessary.

Then I tried just including the BreadcrumbComponent on every page. This method works fine, but also has a huge drawback: I cannot use the functionality of layout pages anymore, because at the layout level, I dont know the content of the BreadcrumbItems, but on the specific pages level, I cannot include the BreadcrumbComponent because then the HTML element would not be the first inside the {children} of the root layout.

And this is my dilemma...

If I understood any concept of Next.js wrong - and the disadvantages are actually non-existent -, please correct me, so I can implement one of the above methods!
However, if the disadvantages are really limitations of Next.js and I'm not wrong, please let me know, if there is a common pattern or design guide for exactly such usecases. I have already ask Chatbots too many times and they only provide non working solutions...

Feel free to discuss as much as you like!

Link to GitHub repo:
https://github.com/Richter-glutenfreie-Produktions-GmbH/batch-management-system
Answered by Ragdoll
Maybe the solution is to create a Client Component, which takes the BreadcrumbItems as props and sets the Context via useEffect.

This idea just hit me and it's great because the page itself doesn't need to be a Client Component, only the smaller component which sets the BreadcrumbContext... 👍
View full answer

6 Replies

Avatar
RagdollOP
Image
Minimalistic code examples (if anyone needs StackBlitz, let me know):

// example-page.tsx
"use client";

import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";

import { BreadcrumbItemType } from "@/components/page-header/app-breadcrumb-navigator";
import AppHeader from "@/components/page-header/app-header";

export default function Page() {
    const [ingredients, setIngredients] = useState<any[] | null>(null);
    const t = useTranslations("ingredients");

    const breadcrumbItems: BreadcrumbItemType[] = [{ name: t("title"), isLast: true }];

    useEffect(() => {
        const getData = async () => {
            const response = await fetch("/api/ingredients");
            const data = await response.json();
            setIngredients(data);
        };
        getData();
    }, []);

    return (
        <>
            <AppHeader breadcrumbItems={breadcrumbItems} />
            <pre>{JSON.stringify(ingredients, null, 2)}</pre>
        </>
    );
}
// app-breadcrumb-navigator.tsx
"use client";
//imports

export type BreadcrumbItemType = { name: string; isLast: true } | { name: string; href?: string; isLast: false };

interface AppBreadcrumbNavigatorProps {
    breadcrumbItems: BreadcrumbItemType[];
}

export default function AppBreadcrumbNavigator({ breadcrumbItems }: AppBreadcrumbNavigatorProps) {
    return (
        <Suspense fallback={<Skeleton className="h-4 w-[200px]" />}>
            {breadcrumbItems.length === 0 ? (
                <Breadcrumb>
                    <BreadcrumbList>
                        <Skeleton className="hidden md:block h-4 w-[150px]" />
                        <BreadcrumbSeparator className="hidden md:block" />
                        <Skeleton className="h-4 w-[100px]" />
                    </BreadcrumbList>
                </Breadcrumb>
            ) : (
                <Breadcrumb>
                    <BreadcrumbList>
                        {breadcrumbItems.map((item) => (
                            <Fragment key={item.name}>
                                <BreadcrumbItem className={item.isLast ? "hidden md:block" : ""}>
                                    {!item.isLast ? (
                                        item.href ? (
                                            <BreadcrumbLink href={item.href}>{item.name}</BreadcrumbLink>
                                        ) : (
                                            item.name
                                        )
                                    ) : (
                                        <BreadcrumbPage>{item.name}</BreadcrumbPage>
                                    )}
                                </BreadcrumbItem>
                                {!item.isLast && <BreadcrumbSeparator className="hidden md:block" />}
                            </Fragment>
                        ))}
                    </BreadcrumbList>
                </Breadcrumb>
            )}
        </Suspense>
    );
}
// layout.tsx
import "../globals.css";
import clsx from "clsx";
import { GeistSans } from "geist/font/sans";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { ThemeProvider } from "next-themes";

import { AppSidebar } from "@/components/app-sidebar";

import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";

const defaultUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000";

export const metadata = {
    metadataBase: new URL(defaultUrl),
    title: "Batch Management System",
    description: "Manage incoming and outgoing batches with ease. Developed for Richter Glutenfreie Backwaren.",
};

export default async function RootLayout({
    children,
    params: { locale },
}: {
    children: React.ReactNode;
    params: { locale: string };
}) {
    const messages = await getMessages();

    return (
        <html lang={locale} className={clsx(GeistSans.className, "h-full")} suppressHydrationWarning>
            <body className="bg-background text-foreground h-full relative">
                <NextIntlClientProvider messages={messages}>
                    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
                        <SidebarProvider>
                            <AppSidebar />
                            <SidebarInset>{children}</SidebarInset>
                        </SidebarProvider>
                    </ThemeProvider>
                </NextIntlClientProvider>
            </body>
        </html>
    );
}
// app-header.tsx
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";

import AppBreadcrumbNavigator, { BreadcrumbItemType } from "./app-breadcrumb-navigator";

interface AppHeaderProps {
    breadcrumbItems: BreadcrumbItemType[];
}

export default function AppHeader({ breadcrumbItems }: AppHeaderProps) {
    return (
        <header className="flex h-16 shrink-0 items-center gap-2">
            <div className="flex items-center gap-2 px-4">
                <SidebarTrigger className="-ml-1" />
                <Separator orientation="vertical" className="mr-2 h-4" />
                <AppBreadcrumbNavigator breadcrumbItems={breadcrumbItems} />
            </div>
        </header>
    );
}
Avatar
RagdollOP
Maybe the solution is to create a Client Component, which takes the BreadcrumbItems as props and sets the Context via useEffect.

This idea just hit me and it's great because the page itself doesn't need to be a Client Component, only the smaller component which sets the BreadcrumbContext... 👍
Answer