🗂

Next.jsで今の場所(current url)を判定して表示を変えるときにつまづいた話

2023/05/05に公開
1

Next.jsで今の場所(current url)を判定して表示を変える
こちらの記事を拝読し実際に実装した際に、私がつまづいた話です。

やりたいこと

現在の場所を判定して、ナビゲーションの該当場所の表示を変えたい
正しく動いているナビゲーションの画像

つまづいたこと

どのページに飛んでも、該当場所に加えて「ホーム」の表示が変わってしまう
間違った動きをしているナビゲーションの画像

解決後コード全文

Nav.jsx
import Link from "next/link";
import { useRouter } from "next/router";
import styles from "./Nav.module.css";

export default function Nav() {
  const router = useRouter();
  const navItems = [
    {
      href: "/",
      title: "ホーム",
    },
    {
      href: "/news",
      title: "お知らせ",
    },
    {
      href: "/about",
      title: "概要",
    },
    {
      href: "/join",
      title: "メンバー募集",
    },
    {
      href: "/contact",
      title: "お問い合わせ",
    },
    {
      href: "/privacy",
      title: "当サイトについて",
    },
  ];

  return (
    <nav>
      <ul>
        {navItems.map((item) => {
          return (
            <li key={item.href}>
              <Link
                href={item.href}
                className={
                  (
                    item.href === "/"
                      ? router.pathname === item.href
                      : router.pathname.startsWith(item.href)
                  )
                    ? styles.menuContentListItemLinkCurrent
                    : styles.menuContentListItemLink
                }
              >
                {item.title}
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

原因

classを切り替える条件について、適切な条件を与えられていなかったことが原因です。
初めは先行記事に則りstartsWith()メソッドのみを使用していました。

router.pathname.startsWith(item.href)
  ? styles.menuContentListItemLinkCurrent
  : styles.menuContentListItemLink

しかしこれは「/news」も「/」で始まるため、予期せぬ場所の表示が変わっていました。

item.href router.pathname result
/ /news true(!?)
/news /news true
/about /news false
/join /news false
/contact /news false
/privacy /news false

解決策

item.hrefが「/」のときは、startsWithの代わりに===(厳密等価演算子)を使って判定するようにして解決です。

- router.pathname.startsWith(item.href)
+ (
+   item.href === "/"
+     ? router.pathname === item.href
+     : router.pathname.startsWith(item.href)
+ )
    ? styles.menuContentListItemLinkCurrent
    : styles.menuContentListItemLink
GitHubで編集を提案

Discussion

nap5nap5

少しデモを作ってみました。

demo code.
https://codesandbox.io/p/sandbox/admiring-hill-843kty?file=%2FREADME.md

demo site.
https://843kty-3000.csb.app/

path-to-regexpライブラリを使って以下のような感じでワークアラウンドするとハンディかもです。

export const navItems: NavItem[] = [
  {
    href: '/',
    title: 'ホーム',
  },
  {
    href: '/news',
    title: 'お知らせ',
  },
  {
    href: '/about',
    title: '概要',
  },
  {
    href: '/join',
    title: 'メンバー募集',
  },
  {
    href: '/contact',
    title: 'お問い合わせ',
  },
  {
    href: '/privacy',
    title: '当サイトについて',
  },
]

export const getCurrentNav = (pathname: string) => {
  return navItems.filter((d) => pathToRegexp(d.href).test(pathname)).pop()
}

ルーティング変化のキャッチハンドリングは以下のようにしてみました。

import { useCallback, useEffect, useMemo, useState } from 'react'

import { useRouter } from 'next/router'

import { getCurrentNav, NavItem } from '@/features/nav/components/Nav'

const useNavigation = () => {
  const router = useRouter()

  const [activeNavItem, setActiveNavItem] = useState<NavItem | undefined>(
    undefined
  )

  const handleRouteChangeStart = useCallback((e: string) => {
    setActiveNavItem(getCurrentNav(e))
  }, [])

  const handleRouteChangeComplete = useCallback((e: string) => {
    setActiveNavItem(getCurrentNav(e))
  }, [])

  useEffect(() => {
    setActiveNavItem(getCurrentNav(router.asPath))
  }, [router])

  useEffect(() => {
    router.events.on('routeChangeStart', handleRouteChangeStart)
    router.events.on('routeChangeComplete', handleRouteChangeComplete)

    return () => {
      router.events.off('routeChangeStart', handleRouteChangeStart)
      router.events.off('routeChangeComplete', handleRouteChangeComplete)
    }
  }, [router, handleRouteChangeStart, handleRouteChangeComplete])

  return useMemo(() => {
    return {
      activeNavItem,
    }
  }, [activeNavItem])
}

export default useNavigation

簡単ですが、以上です。