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?
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.