7
votes

I'm currently creating a multilevel sidebar menu in REACT and I want to dynamically add active class to parent <li> when the current route is active.

The code below is how I do it for a single menu.

<Route
  path={this.props.to}
  exact={this.props.activeOnlyWhenExact}
  children={({ match }) => (
    <li className={match ? "active" : ""}>
      <Link to={this.props.to} activeClassName="active">
        <span className="sidebar-mini-icon">
          <i className={this.props.icon}></i></span>
          {this.props.label}
      </Link>
    </li>
  )}
/>

Now I wonder how I do it with multilevel menu. My menu looks like this;

enter image description here

As you can see, only the menu has the active class. Here's my code for the multi level menu:

<Route
  path={this.props.to}
  exact={this.props.activeOnlyWhenExact}
  children={({ match }) => (
  <li>
    <a data-toggle="collapse" href="#Collapse">
      <span className="sidebar-mini-icon">
        <i className={this.props.icon}></i>
      </span>
      <span className="sidebar-normal">
        {this.props.name}
        <b className="caret"></b>
      </span>
    </a>
    <div class="collapse" id="Collapse">
       <ul className="nav">
         {this.props.children}
       </ul>
    </div>
  </li>
  )}
/>

I'm using React Router. Any help will be appreciated. Thank you.

4
I am looking for a similar answer too. Please help someone.umanga shrestha

4 Answers

0
votes

My first instinct would be to add className={match ? "active" : ""} to the parent li, but since you're asking about it, I'm assuming you already tried that.

You can dig around in the children's props to find out if any of them have the match prop (or any other prop that would indicate they're active). The following function can find a match prop from the children.

const childMatch = children => {
  if (!children) { return false }
  if (Array.isArray(children)) {
    return children.reduce(
      (acc, child) => acc || Boolean(child.props.match),
      false
    )
  }
  return Boolean(children.props.match)
}

You could then use it like className={childMatch(this.props.children) ? "active" : ""}.

If the prop you're looking for is deeper down the children tree, you can of course dig deeper. You can find grandchildren as child.props.children.

0
votes

You can use React router's nav link

<NavLink to="/faq" activeClassName="selected">
  FAQs
</NavLink>
0
votes

You can use the useLocation hook.

With the useEffect hook, you can run a function whenever, the location changes. That way you add the class to the active parent on every location change.

But you'll need to make sure that the useLocation call is not in the same component that puts the router into the DOM.

So in the root index file, or some other component, you can have something like this

import { useLocation, useEffect } from "react-router-dom";

const Component = () => {
  const location = useLocation();

  useEffect(() => {
      // Check if there is already an element with the activeParentClassName, and remove that so there won't be two clashing elements
      document.querySelector(activeParentClassName)?.classList.remove(activeParentClassName);

      // Select the nav item with the active class, and add the extra class to its parent
      document.querySelector(activeClassName)?.parentElement?.classList.add(activeParentClassName);
  }, [location])
  
  return (...);
}
0
votes

The solution would depend on your route configuration. If you are using non-exact routes, the solution with NavLink would be the most applicable.

Example:

routes.jsx

<Route path="/parent/child-a">
  <ChildA />
</Route>
<Route path="/parent/child-b">
  <ChildB />
</Route>
<Route path="/parent">
  <Parent />
</Route>

Using the above structure, /parent/child-a would match two routes (/parent and /parent/child-a) and the activeClassName prop on the NavLink would be applied on both HTML element.

The "challenge" with the above structure is that you are going to render <Parent /> in all three cases.

This could be solved by using nested/child routes.

routes.jsx

<Route path="/parent">
  <NestedRoutes />
</Route>

nestedRoutes.jsx

<Route path="/parent/child-a" exact>
  <ChildA />
</Route>
<Route path="/parent/child-b" exact>
  <ChildB />
</Route>

With the updated structure above, /parent/child-a would match the top-level route (and as a result add the activeClassName to your HTML element) and also match the child route and add the activeClassName there too.

Note that if you need /parent to also work as a route, you need to update your nested routes as follows:

nestedRoutes.jsx

<Route path="/parent" exact>
  <Parent />
</Route>

From my experience, if you find yourself handling these cases manually through JavaScript or useLocation you might want to revisit your route configuration. react-router is an excellent library in its simplicity and will probably handle your case out of the box.