Search code examples
cssreactjstypescriptreact-routerreact-router-dom

How to set active class in React-Router?


I have a navigation created using React-Router

nav.tsx

import React from 'react'
import { menu } from './menu'
import { Link } from 'react-router-dom'
import styles from './HamburgerMenu.module.scss'

const HamburgerMenu: React.FC = () => {
  const [active, setActive] = React.useState<number>(0)
  console.log(active)

  return (
    <nav>
      <ul className={styles.menu}>
        {menu.map((item, index) => (
          <li
            onClick={() => setActive(index)}
            key={item.title}
            className={styles[active === index ? 'active' : '']}
          >
            <Link to={item.link}>{item.title}</Link>
          </li>
        ))}
      </ul>
    </nav>
  )
}

export default HamburgerMenu

menu.ts:

export const menu: IMenuItem[] = [
  {
    link: '/',
    title: 'главная'
  },
  {
    link: '/profile',
    title: 'профиль'
  },
  {
    link: '/discipline',
    title: 'наказания'
  },
  {
    link: '/store',
    title: 'магазин'
  },
  {
    link: '/statistics',
    title: 'статистика'
  }
]

main.tsx:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
import Home from './pages/Home/Home'
import './assets/style/global.scss'
import NotFoundPage from './pages/NotFoundPage'
import Profile from './pages/Profile/Profile'

const router = createBrowserRouter([
  {
    element: <Home />,
    path: '/'
  },
  {
    element: <Profile />,
    path: '/profile'
  },
  {
    element: <NotFoundPage />,
    path: '*'
  }
])

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

style.scss

.menu {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10;
    margin-right: 100px;

    li.nav-link-item {
        margin: 0px 50px;
        a {
            color: $white;
            opacity: 0.7;
            transition: opacity 0.3s ease;
            font-size: 20px;

            &:hover {
                opacity: 1;
            }
        }
    }

    li.nav-link-item:has(a.active) {
        border-radius: 10px;
        border: 5px solid #542c44;
        padding: 7px 42px;
    }
}

Layout.tsx - the page turns into it

import React from 'react'
import { ILayoutProps } from './Laout.type'
import Header from '../Header/Header'
import Footer from '../Footer/Footer'
import styles from './Layout.module.scss'

const Layout: React.FC<ILayoutProps> = ({ children }) => {
    return (
        <div className={styles['layout']}>
            <Header />
            {children}
            <Footer />
        </div>
    )
}

export default Layout

When switching between '/' and '/profile' the "active" class is constantly reset and switched to 0, if I want the "active" class to be added to '/profile', then I need to click on it 2 times, and if I switch between '/discipline', '/store', '/statistics', classes are assigned normally. I understand that this behavior is most likely due to the fact that '/discipline', '/store', '/statistics' are not declared pages, but what about '/' and '/profile'? Why could this be happening, and how can this be corrected?


Solution

  • Use the NavLink component which applies an "active" classname for the matched link by default, and use CSS to select the li element with a currently matching link.

    Example:

    const HamburgerMenu: React.FC = () => {
      return (
        <nav>
          <ul className={styles.menu}>
            {menu.map((item, index) => (
              <li
                key={item.title}
                className="nav-link-item" // <-- something easily targetable
              >
                <NavLink end to={item.link}>
                  {item.title}
                </NavLink>
              </li>
            ))}
          </ul>
        </nav>
      );
    };
    
    li.nav-link-item {
      /* li styling */
    }
    
    li.nav-link-item:has(a.active) {
      /* "active" li styling overrides */
    }
    

    Information:

    Demo

    Edit how-to-set-active-class-in-react

    enter image description here

    Since you are using SASS, this appears to be correct CSS for what you are working with:

    li.nav-link-item {
      margin: 0px 50px;
    
      a {
        color: #fff;
        opacity: 0.7;
        transition: opacity 0.3s ease;
        font-size: 20px;
    
        &:hover {
          opacity: 1;
        }
      }
    
      &:has(:global(a.active)) { // <-- access global non-SASS "active" class
        border-radius: 10px;
        border: 5px solid #542c44;
        padding: 7px 42px;
      }
    }
    

    enter image description here