Skip to content

refactor: replace router.push with next/link in SidebarItem#562

Open
miyaji255 wants to merge 2 commits intonetbirdio:mainfrom
miyaji255:refactor/sidebar-navigation-link
Open

refactor: replace router.push with next/link in SidebarItem#562
miyaji255 wants to merge 2 commits intonetbirdio:mainfrom
miyaji255:refactor/sidebar-navigation-link

Conversation

@miyaji255
Copy link
Copy Markdown

@miyaji255 miyaji255 commented Feb 17, 2026

This pull request refactors the SidebarItem component to improve navigation handling and accessibility. The most significant changes include replacing custom navigation logic with Next.js's Link component, refining click handling, and improving the structure for collapsible sidebar items.

Navigation and click handling improvements:

  • Replaced manual routing using router.push with Next.js's Link component for navigation, ensuring better accessibility and client-side routing. (src/components/SidebarItem.tsx) [1] [2]
  • Refined the handleClick function to prevent navigation when the item is already active and to handle special cases for collapsible items and external links. (src/components/SidebarItem.tsx)

Component structure and rendering:

  • Refactored the rendering logic to separate the clickable content from the container, and conditionally render a <Link> or <button> based on props, improving semantics and accessibility. (src/components/SidebarItem.tsx) [1] [2]

Related: #561

Summary by CodeRabbit

  • Refactor

    • Improved sidebar navigation behavior: links are handled more reliably, redundant navigation is prevented, and mobile navigation toggles correctly.
  • Style

    • Preserved and refined active-state styling and collapsible chevrons for clearer visual hierarchy and interaction feedback.
  • Bug Fixes

    • Ensures external/open-in-new-tab links and collapsible items behave consistently.

Copilot AI review requested due to automatic review settings February 17, 2026 01:34
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

Warning

Rate limit exceeded

@miyaji255 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 24 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Refactors SidebarItem.tsx to use Next.js Link for non-collapsible items, removing useRouter. Item rendering now conditionally wraps content with Link or a button, computes isActive, refines click handling (prevents navigation when active, respects target="_blank"), and preserves collapsible behavior and chevron logic.

Changes

Cohort / File(s) Summary
Sidebar Navigation Refactor
src/components/SidebarItem.tsx
Replaces useRouter() navigation with Next.js Link for non-collapsible items; extracts item content into a reusable block; adds isActive memo and refined handleClick to prevent redundant navigation and respect target="_blank"; preserves Radix-based collapsible trigger/content and existing styling/chevron logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I hopped through links and chevrons bright,

Replaced a push with gentler light,
Buttons that toggle, Links that roam,
Sidebar snug now finds its home,
A tiny hop, and code feels right.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly describes the main change: replacing router.push with next/link in the SidebarItem component.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the SidebarItem component to replace imperative navigation using router.push with Next.js's declarative Link component, addressing performance and accessibility concerns identified in issue #561.

Changes:

  • Replaced manual routing with Next.js Link component for better prefetching and SPA-like navigation
  • Refactored rendering logic to conditionally render Link or button based on whether the item is collapsible
  • Improved click handler to prevent default behavior for active items and handle edge cases
Comments suppressed due to low confidence (1)

src/components/SidebarItem.tsx:176

  • There's a structural issue with the Collapsible component usage. All sidebar items are wrapped in Collapsible.Root and Collapsible.Trigger, even when collapsible={false}. For non-collapsible items (the majority), this adds unnecessary DOM structure and potentially unwanted behavior. The Collapsible.Trigger with asChild will delegate its trigger behavior to the child element (Link or button), which means non-collapsible items will have collapsible trigger behavior even though there's no content to collapse. Consider conditionally rendering the Collapsible wrapper only when collapsible={true}:
if (collapsible) {
  return (
    <Collapsible.Root open={open} onOpenChange={setOpen}>
      <Collapsible.Trigger asChild>
        <li>...</li>
      </Collapsible.Trigger>
      <Collapsible.Content>{children}</Collapsible.Content>
    </Collapsible.Root>
  );
} else {
  return <li>...</li>;
}
  return (
    <Collapsible.Root open={open} onOpenChange={setOpen}>
      <Collapsible.Trigger asChild>
        <li className={"px-3 cursor-pointer list-none"}>
          {href && !collapsible ? (
            <Link
              href={href}
              target={target}
              onClick={handleClick}
            >
              {content}
            </Link>
          ) : (
            <button
              type="button"
              className={"w-full"}
              onClick={handleClick}
              disabled={href === ""}
            >
              {content}
            </button>
          )}
        </li>
      </Collapsible.Trigger>
      {collapsible && <Collapsible.Content>{children}</Collapsible.Content>}
    </Collapsible.Root>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/SidebarItem.tsx (1)

151-176: ⚠️ Potential issue | 🟠 Major

Collapsible.Trigger adds aria-expanded to non-collapsible link items.

Since Collapsible.Root / Collapsible.Trigger asChild always wraps the <li>, non-collapsible <Link> items receive aria-expanded and data-state attributes from Radix. Screen readers will announce these links as expandable toggles, which undermines the accessibility goal of this PR.

Consider conditionally wrapping with Collapsible only when collapsible is true:

♻️ Sketch
-  return (
-    <Collapsible.Root open={open} onOpenChange={setOpen}>
-      <Collapsible.Trigger asChild>
-        <li className={"px-3 cursor-pointer list-none"}>
-          {href && !collapsible ? (
-            <Link
-              href={href}
-              target={target}
-              onClick={handleClick}
-            >
-              {content}
-            </Link>
-          ) : (
-            <button
-              type="button"
-              className={"w-full"}
-              onClick={handleClick}
-              disabled={href === ""}
-            >
-              {content}
-            </button>
-          )}
-        </li>
-      </Collapsible.Trigger>
-      {collapsible && <Collapsible.Content>{children}</Collapsible.Content>}
-    </Collapsible.Root>
-  );
+  if (collapsible) {
+    return (
+      <Collapsible.Root open={open} onOpenChange={setOpen}>
+        <Collapsible.Trigger asChild>
+          <li className={"px-3 cursor-pointer list-none"}>
+            <button
+              type="button"
+              className={"w-full"}
+              onClick={handleClick}
+              disabled={href === ""}
+            >
+              {content}
+            </button>
+          </li>
+        </Collapsible.Trigger>
+        <Collapsible.Content>{children}</Collapsible.Content>
+      </Collapsible.Root>
+    );
+  }
+
+  return (
+    <li className={"px-3 cursor-pointer list-none"}>
+      {href ? (
+        <Link href={href} target={target} onClick={handleClick}>
+          {content}
+        </Link>
+      ) : (
+        <button
+          type="button"
+          className={"w-full"}
+          onClick={handleClick}
+          disabled
+        >
+          {content}
+        </button>
+      )}
+    </li>
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SidebarItem.tsx` around lines 151 - 176, The current
implementation wraps every item in Collapsible.Root/Collapsible.Trigger causing
Radix props like aria-expanded/data-state to be applied to non-collapsible
links; update SidebarItem so it only uses Collapsible.Root, Collapsible.Trigger,
and Collapsible.Content when the prop collapsible is true—if collapsible is
false render the <li> with the <Link> or <button> directly (preserve existing
handleClick, href, target, disabled logic and content) to avoid passing Radix
attributes to non-collapsible items; keep the children rendering inside
Collapsible.Content only in the collapsible branch.
🧹 Nitpick comments (1)
src/components/SidebarItem.tsx (1)

84-93: Consider using cn consistently instead of mixing classNames and cn.

Lines 86, 106 use classNames() while lines 115, 129, 139 use cn(). Since cn wraps twMerge(clsx(...)) and correctly resolves Tailwind class conflicts, it's the safer choice for all class merging in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SidebarItem.tsx` around lines 84 - 93, The component builds
its JSX class strings using classNames in the const content block but uses cn
elsewhere; replace classNames(...) with cn(...) so all class merging uses the
twMerge+clsx wrapper (e.g., change the classNames call inside the const content
JSX to cn) and ensure the existing cn import is used (remove classNames import
if now unused) so Tailwind conflicts are consistently resolved across
SidebarItem (refer to the const content and other JSX class attribute usages).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/SidebarItem.tsx`:
- Around line 155-162: The Link JSX in SidebarItem renders external links with
target but doesn't add rel="noopener noreferrer"; update the Link element
(inside the SidebarItem component where href, target, handleClick and content
are used) to include a rel attribute when target === "_blank" (e.g., set rel to
"noopener noreferrer" only for blank targets) so opened tabs cannot access
window.opener while preserving current behavior for other targets.
- Around line 59-70: The isActive computation is duplicated in handleClick; move
the useMemo that computes isActive (the memo using exactPathMatch, path and
href) above handleClick and reuse that memoized isActive inside handleClick
instead of recomputing; change any loose equality (==) to strict equality (===)
in the memo and where used (e.g., comparisons of path and href), and simplify
the collapsible guard to rely on collapsible alone (e.g., if (collapsible)
return;) since collapsible items always render a button and Collapsible.Trigger
handles toggling—update handleClick to call e.preventDefault() when the memoized
isActive is true.

---

Outside diff comments:
In `@src/components/SidebarItem.tsx`:
- Around line 151-176: The current implementation wraps every item in
Collapsible.Root/Collapsible.Trigger causing Radix props like
aria-expanded/data-state to be applied to non-collapsible links; update
SidebarItem so it only uses Collapsible.Root, Collapsible.Trigger, and
Collapsible.Content when the prop collapsible is true—if collapsible is false
render the <li> with the <Link> or <button> directly (preserve existing
handleClick, href, target, disabled logic and content) to avoid passing Radix
attributes to non-collapsible items; keep the children rendering inside
Collapsible.Content only in the collapsible branch.

---

Nitpick comments:
In `@src/components/SidebarItem.tsx`:
- Around line 84-93: The component builds its JSX class strings using classNames
in the const content block but uses cn elsewhere; replace classNames(...) with
cn(...) so all class merging uses the twMerge+clsx wrapper (e.g., change the
classNames call inside the const content JSX to cn) and ensure the existing cn
import is used (remove classNames import if now unused) so Tailwind conflicts
are consistently resolved across SidebarItem (refer to the const content and
other JSX class attribute usages).

@miyaji255 miyaji255 force-pushed the refactor/sidebar-navigation-link branch from 92a51b9 to 13ec058 Compare February 17, 2026 01:49
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Feb 17, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/SidebarItem.tsx`:
- Around line 148-170: The button in itemContent is being disabled whenever href
=== "" which prevents clicks from reaching Collapsible.Trigger and breaks
expand/collapse for items where collapsible === true; update the disabled prop
on the <button> (in the SidebarItem component where itemContent and handleClick
are defined) to only disable when the item is non-collapsible and has no href
(i.e., disabled should be true only when !collapsible && href === ""); this
preserves pointer events for collapsible items while still disabling
non-interactive, non-collapsible buttons.

---

Duplicate comments:
In `@src/components/SidebarItem.tsx`:
- Around line 151-158: Confirm that the Next.js Link component sets rel when
target is "_blank": keep the Link usage as written (Link with href={href},
target={target}, rel={target === "_blank" ? "noopener noreferrer" : undefined},
onClick={handleClick}) to ensure external links get rel="noopener noreferrer";
no code change required, but ensure this exact conditional remains on the Link
definition (refer to Link, href, target, rel, handleClick, content).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants