Open6

Next.js(AppRouter)で、PathによってActive状態が変わるボトムナビゲーションバーの作成

ふくえもんふくえもん

第一段階

'use client'

import { FC } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

type Item = {
  path: string
  label: string
}

type BottomNavbarProps = {
  items: Item[]
  path: string
}

const BottomNavbar: FC<BottomNavbarProps> = ({ items, path }) => {
  return (
    <ul>
      {items.map((item) => {
        const isActive = item.path === path

        return (
          <li key={item.path} className={isActive ? 'font-bold' : ''}>
            <Link href={'/sample/blog/${item.path}'}>{item.label}</Link>
          </li>
        )
      })}
    </ul>
  )
}

export const BottomNavbarContainer = () => {
  const path = usePathname()
  const items: Item[] = [
    { path: '/', label: 'Home' },
    { path: '/shop/search', label: 'PostCreate' },
    { path: '/profile', label: 'Profile' }
  ]
  return <BottomNavbar items={items} path={path} />
}

アイコンとpathを渡してpropsとして渡す
iconのpathと現在のurlのpathが一致していたら、active状態になる

ふくえもんふくえもん

StorybookでNavbarを見るとこんな感じ

Pathと一致するとStyleが変わる

import { StoryObj, Meta } from '@storybook/react'
import { BottomNavbar } from './BottomNavbar'

const meta: Meta<typeof BottomNavbar> = {
  component: BottomNavbar,
  argTypes: {
    items: {
      control: {
        type: 'object'
      }
    }
  }
}

const items = [
  { label: 'Home', path: '/' },
  { label: 'Post', path: 'shop/search' },
  { label: 'Profile', path: 'profilet' }
]

export default meta

type Story = StoryObj<typeof BottomNavbar>

export const Default: Story = {
  args: {
    items
  }
}
ふくえもんふくえもん

Styleを当てた

コード
export const BottomNavbar: FC<BottomNavbarProps> = ({ items, path }) => {
  return (
    <div className="md:disabled: fixed bottom-0 left-0 z-50 flex h-16 w-full items-center justify-center border bg-white">
      <ul className="relative flex w-full justify-between px-4">
        {items.map((item) => {
          const isActive = item.path === path
          return (
            <li
              key={item.path}
              className={cn('flex flex-col items-center justify-center text-center mx-4', {
                'font-bold': isActive
              })}
            >
              <Link href={item.path} className="flex flex-col items-center justify-center text-sm ">
                <span
                  className={cn('text-3xl text-text transition-transform transform', {
                    'font-extrabold text-red scale-110': isActive // アクティブな状態の色変更、サイズ拡大
                  })}
                >
                  {item.icon}
                </span>
                <span
                  className={cn({
                    'border-b-2 border-red': isActive // アクティブな状態の下線
                  })}
                >
                  {item.label}
                </span>
              </Link>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

ふくえもんふくえもん

これだと、一つのpathに対応できない
例:shop/search -> post/create に遷移する場合

ふくえもんふくえもん

解決策1
対応するpathを配列上で受け取るようにする

'use client'
import React, { FC } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { AiOutlineHome } from 'react-icons/ai'
import { AiOutlineUser } from 'react-icons/ai'
import { PiNotePencil } from 'react-icons/pi'
import { cn } from '@/libs/tailwind/utils'

type NavbarItem = {
  paths: string[] //配列にする
  label: string
  icon: React.ReactNode
}

type BottomNavbarProps = {
  items: NavbarItem[]
  path: string
}

export const BottomNavbar: FC<BottomNavbarProps> = ({ items, path }) => {
  return (
    <div className="md:disabled: fixed bottom-0 left-0 z-50 flex h-20 w-full items-center justify-center border bg-white">
      <ul className="relative flex w-full justify-between px-4">
        {items.map((item) => {
          const isActive = item.paths.includes(path)
          return (
            <li
              key={item.paths[0]}
              className={cn('flex flex-col items-center justify-center text-center mx-4 ', {
                'font-bold': isActive
              })}
            >
              <Link href={item.paths[0]} className="flex flex-col items-center justify-center text-sm ">
                {/* アイコン */}
                <span
                  className={cn('text-3xl text-text transition-transform transform', {
                    'font-extrabold text-red scale-110': isActive
                  })}
                >
                  {item.icon}
                </span>
                {/* ラベル */}
                <span
                  className={cn({
                    'border-b-2': isActive // アクティブな状態の下線
                  })}
                >
                  {item.label}
                </span>
              </Link>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

export const BottomNavbarContainer = () => {
  const path = usePathname()
  const items: NavbarItem[] = [ 
    { paths: ['/'], label: 'Home', icon: <AiOutlineHome /> },
    { paths: ['/shop/search', '/post/create'], label: 'Post', icon: <PiNotePencil /> }, //対応するpathを配列上で用意する
    { paths: ['/profile'], label: 'Profile', icon: <AiOutlineUser /> }
  ]
  return <BottomNavbar items={items} path={path} />
}

応急処置感がすごい
❌これだとpathが増えた時にいちいち増やさないといけない