Skip to content

Commit edb5a28

Browse files
committed
Cleanup NavLink. No need for internal state
1 parent 4e84b5a commit edb5a28

File tree

1 file changed

+40
-43
lines changed

1 file changed

+40
-43
lines changed

src/app/_layout/Header/NavLink.tsx

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,55 @@
11
"use client";
22

3-
import Link from "next/link";
3+
import Link, { type LinkProps } from "next/link";
44
import { usePathname } from "next/navigation";
5-
import {
6-
type AnchorHTMLAttributes,
7-
forwardRef,
8-
useEffect,
9-
useState,
10-
} from "react";
5+
import { type ComponentPropsWithoutRef, forwardRef } from "react";
6+
import type { UrlObject } from "url";
117
import { cva } from "^/lib/cva";
128

139
const navLinkVariants = cva(
1410
"block rounded-md transition-all duration-200 focus:outline-none",
1511
{
1612
variants: {
17-
isActive: {
18-
// 40% opacity
19-
true: "opacity-40",
20-
},
13+
isActive: { true: "opacity-40" },
2114
},
2215
},
2316
);
2417

25-
export interface NavLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {}
26-
27-
const NavLink = forwardRef<HTMLAnchorElement, NavLinkProps>(
28-
({ href = "", className, ...props }, ref) => {
29-
const asPath = usePathname();
30-
const [isActive, setIsActive] = useState(false);
31-
32-
useEffect(() => {
33-
if (asPath != null && location != null) {
34-
// eslint-disable-next-line react-hooks/set-state-in-effect
35-
setIsActive(
36-
new URL(href, location.href).pathname ===
37-
new URL(asPath, location.href).pathname,
38-
);
39-
}
40-
41-
return () => {
42-
setIsActive(false);
43-
};
44-
}, [asPath, href]);
45-
46-
return (
47-
<Link
48-
href={href}
49-
className={navLinkVariants({ isActive, className })}
50-
{...props}
51-
ref={ref}
52-
/>
53-
);
54-
},
55-
);
56-
NavLink.displayName = "NavLink";
18+
export type NavLinkProps = ComponentPropsWithoutRef<typeof Link>;
19+
20+
function normalizePathname(path: string): string {
21+
if (!path) return "/";
22+
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
23+
}
24+
25+
function hrefToPathname(href: LinkProps["href"]): string {
26+
if (typeof href === "string") {
27+
// Extract pathname from "/x?y=1#z" safely
28+
return new URL(href, "http://n").pathname;
29+
}
30+
return (href as UrlObject).pathname ?? "/";
31+
}
32+
33+
const NavLink = forwardRef<HTMLAnchorElement, NavLinkProps>(function NavLink(
34+
{ href, className, ...props },
35+
ref,
36+
) {
37+
const pathname = usePathname();
38+
39+
const current = normalizePathname(pathname);
40+
const target = normalizePathname(hrefToPathname(href));
41+
42+
const isActive = current === target;
43+
44+
return (
45+
<Link
46+
ref={ref}
47+
href={href}
48+
className={navLinkVariants({ isActive, className })}
49+
aria-current={isActive ? "page" : undefined}
50+
{...props}
51+
/>
52+
);
53+
});
5754

5855
export default NavLink;

0 commit comments

Comments
 (0)