javascriptreactjsnext.js

How Can I Make the Active Sidebar Item Reflect the Current URL in Next.js?


I'm working on a Next.js project and I'm trying to ensure that the active sidebar item correctly reflects the current URL. I've attempted to use the useRouter hook in combination with useEffect within a functional component. However, I'm encountering an issue where the sidebar item does not update as expected. Additionally, I'm receiving an error stating NextRouter was not mounted.

Here's the code snippet I'm working with:

"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import "@/app/globals.css";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import {
  Package,
  SquareArrowUpRight,
  HandCoins,
  Users,
  ScanEye,
  LayoutDashboard,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export default function Sidebar() {
  const [activeItem, setActiveItem] = useState(0);
  const handleItemClick = (index) => setActiveItem(index);
  const allItemClass =
    "flex items-center gap-3 rounded-lg px-3 py-2 transition-all";
  const activeItemClass =
    "bg-primary text-white dark:bg-primary dark:text-black";
  const inactiveItemClass = "text-muted-foreground hover:text-primary";
  const { setTheme } = useTheme();

  // Ensure theme is set on the client side
  useEffect(() => {
    setTheme("system");
  }, [setTheme]);

  return (
    <div className="flex min-h-screen w-1/4">
      <div className="hidden border-r bg-muted/40 md:block">
        <div className="flex h-full max-h-screen flex-col gap-2">
          <div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
            <Link
              href="/dashboard"
              className="flex items-center gap-2 font-semibold"
            >
              <ScanEye className="h-6 w-6" />
              <span className="">Reebews</span>
            </Link>
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button
                  variant="outline"
                  size="icon"
                  className="ml-auto w-8 h-8"
                >
                  <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
                  <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
                  <span className="sr-only">Toggle theme</span>
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                <DropdownMenuItem onClick={() => setTheme("light")}>
                  Light
                </DropdownMenuItem>
                <DropdownMenuItem onClick={() => setTheme("dark")}>
                  Dark
                </DropdownMenuItem>
                <DropdownMenuItem onClick={() => setTheme("system")}>
                  System
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </div>

          <div className="flex-1">
            <nav className="grid items-start px-2 text-sm font-medium lg:px-4">
              <Link
                href="/dashboard"
                className={`${allItemClass} ${
                  activeItem === 0 ? activeItemClass : inactiveItemClass
                }`}
                onClick={() => handleItemClick(0)}
              >
                <LayoutDashboard className="h-4 w-4" />
                Dashboard
              </Link>
              <Link
                href="/campaigns"
                className={`${allItemClass} ${
                  activeItem === 1 ? activeItemClass : inactiveItemClass
                }`}
                onClick={() => handleItemClick(1)}
              >
                <SquareArrowUpRight className="h-4 w-4" />
                Campaign Manager
              </Link>
              <Link
                href="/products"
                className={`${allItemClass} ${
                  activeItem === 2 ? activeItemClass : inactiveItemClass
                }`}
                onClick={() => handleItemClick(2)}
              >
                <Package className="h-4 w-4" />
                Products{" "}
              </Link>
              <Link
                href="/rewards-center"
                className={`${allItemClass} ${
                  activeItem === 3 ? activeItemClass : inactiveItemClass
                }`}
                onClick={() => handleItemClick(3)}
              >
                <HandCoins className="h-4 w-4" />
                Rewards Center
              </Link>
              <Link
                href="/customer-list"
                className={`${allItemClass} ${
                  activeItem === 4 ? activeItemClass : inactiveItemClass
                }`}
                onClick={() => handleItemClick(4)}
              >
                <Users className="h-4 w-4" />
                Customer List
              </Link>
            </nav>
          </div>
          <div className="mt-auto p-4">
            <Card x-chunk="dashboard-02-chunk-0">
              <CardHeader className="p-2 pt-0 md:p-4">
                <CardTitle>Upgrade to Pro</CardTitle>
                <CardDescription>
                  Unlock all features and get unlimited access to our support
                  team.
                </CardDescription>
              </CardHeader>
              <CardContent className="p-2 pt-0 md:p-4 md:pt-0">
                <Button size="sm" className="w-full">
                  Upgrade
                </Button>
              </CardContent>
            </Card>
          </div>
        </div>
      </div>
    </div>
  );
}

How can I correctly synchronize the active sidebar item with the current URL to ensure that the active item is highlighted appropriately?


Solution

  • You can achieve that using the NextJS' usePathname hook. You can match your active url against your navigation menu and determine the active menu from that. I have replicated your code on my end and I have also made a huge refactoring to your code as there are too many duplicates in your code. In other to help keep your code DRY, I have made the following refactoring:

    //@/components/layout/NavItem.js
    
    "use client";
    
    import Link from "next/link";
    import { usePathname } from "next/navigation";
    
    
    export default function NavItem({ label, Icon, path, onClick }) {
      const pathname = usePathname();
    
      return (
        <Link
          href={path}
          className={cn(
            "flex items-center gap-3 rounded-lg px-3 py-2 transition-all text-muted-foreground hover:text-primary",
            {
              "bg-primary text-white dark:bg-primary dark:text-black":
                pathname === path,
            }
          )}
          onClick={onClick}
        >
          <Icon className="h-4 w-4" />
          {label}
        </Link>
      );
    }
    
    

    I have abstracted your individual nav menu into a separate component and applied the active and inactive classes to the component.

    "use client";
    import React, {  useEffect } from "react";
    import Link from "next/link";
    import "@/app/globals.css";
    import { Moon, Sun } from "lucide-react";
    import { useTheme } from "next-themes";
    import {
      Package,
      SquareArrowUpRight,
      HandCoins,
      Users,
      ScanEye,
      LayoutDashboard,
    } from "lucide-react";
    import { Button } from "@/components/ui/button";
    import {
      Card,
      CardContent,
      CardDescription,
      CardHeader,
      CardTitle,
    } from "@/components/ui/card";
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuTrigger,
    } from "@/components/ui/dropdown-menu";
    import NavItem from "./nav-item";
    
    const sideMenus = [
      { label: "Dashboard", path: "/dashboard", Icon: LayoutDashboard },
      { label: "Campaign Manager", path: "/campaigns", Icon: SquareArrowUpRight },
      { label: "Products", path: "/products", Icon: Package },
      { label: "Rewards Center", path: "/rewards-center", Icon: HandCoins },
      { label: "Customer List", path: "/customer-list", Icon: Users },
    ];
    
    export default function Sidebar() {
      const { setTheme } = useTheme();
    
      // Ensure theme is set on the client side
      useEffect(() => {
        setTheme("system");
      }, [setTheme]);
    
      return (
        <div className="flex min-h-screen w-1/4">
          <div className="hidden border-r bg-muted/40 md:block">
            <div className="flex h-full max-h-screen flex-col gap-2">
              <div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
                <Link
                  href="/dashboard"
                  className="flex items-center gap-2 font-semibold"
                >
                  <ScanEye className="h-6 w-6" />
                  <span className="">Reebews</span>
                </Link>
                <DropdownMenu>
                  <DropdownMenuTrigger asChild>
                    <Button
                      variant="outline"
                      size="icon"
                      className="ml-auto w-8 h-8"
                    >
                      <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
                      <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
                      <span className="sr-only">Toggle theme</span>
                    </Button>
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="end">
                    <DropdownMenuItem onClick={() => setTheme("light")}>
                      Light
                    </DropdownMenuItem>
                    <DropdownMenuItem onClick={() => setTheme("dark")}>
                      Dark
                    </DropdownMenuItem>
                    <DropdownMenuItem onClick={() => setTheme("system")}>
                      System
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
              </div>
    
              <div className="flex-1">
                <nav className="grid items-start px-2 text-sm font-medium lg:px-4">
                  {sideMenus.map((menu) => (
                    <NavItem key={menu.label} {...menu} />
                  ))}
                </nav>
              </div>
              <div className="mt-auto p-4">
                <Card x-chunk="dashboard-02-chunk-0">
                  <CardHeader className="p-2 pt-0 md:p-4">
                    <CardTitle>Upgrade to Pro</CardTitle>
                    <CardDescription>
                      Unlock all features and get unlimited access to our support
                      team.
                    </CardDescription>
                  </CardHeader>
                  <CardContent className="p-2 pt-0 md:p-4 md:pt-0">
                    <Button size="sm" className="w-full">
                      Upgrade
                    </Button>
                  </CardContent>
                </Card>
              </div>
            </div>
          </div>
        </div>
      );
    }
    
    

    Also, I have computed your navigation dataset into an array called sideMenus and the individual array element is mapped to the NavItem component. I hope this works for you.