BlackMath
BlackMath

Reputation: 992

Toggles a class on an element when another element is clicked React

I have this component in react. I would like whenever I click an a tag with class 'btn' to add/toggle a class 'open' to div with class 'smenu' within the same li element. I implemented it naively like the following, but I am sure there should be another more efficient way. Any tips would be appreciated. Thanks in advance

import React, { useState } from "react";

const AccordioMenu = () => {
  const [activeP, setActiveP] = useState(false);
  const [activeM, setActiveM] = useState(false);

  const toggleActiveP = () => {
    setActiveP(!activeP);
  };
  const toggleActiveM = () => {
    setActiveM(!activeM);
  };

  let btnclassesP = ['smenu']
  if(activeP){
    btnclassesP.push('open')
  }
  let btnclassesM = ['smenu']
  if(activeM){
    btnclassesM.push('open')
  }
  return (
    <div className="middle">
      <div className="menu">
        <li className="item" id="profile">
          <a className='btn' href="#" onClick={toggleActiveP}>
            Profile
          </a>
          <div className={btnclassesP.join(' ')}>
            <a href="">Posts</a>
            <a href="">Pictures</a>
          </div>
        </li>
        <li className="item" id="messages">
          <a className="btn" href="#" onClick={toggleActiveM}>
            Messages
          </a>
          <div className={btnclassesM.join(' ')}>
            <a href="">New</a>
            <a href="">Sent</a>
          </div>
        </li>
        <li className="item" id="logout">
          <a className="btn" href="#">
            Logout
          </a>
        </li>
      </div>
    </div>
  );
};

export default AccordioMenu;

Upvotes: 1

Views: 86

Answers (2)

Cat_Enthusiast
Cat_Enthusiast

Reputation: 15688

If you want to simplify this even further you could just use one-state value, that way the component has a single source of truth to share.

Let's have a state that stores an array of identifiers. Each identifier is associated with a different set of links. We'll use "message" and "profile" as the identifiers. Naturally, if there is nothing in the array, then all sub-links should be collapsed.

Then we can use just an event-handler to add/remove the identifier into the state array. Lastly, we can use an inline-style to determine whether that set of links corresponding to the identifier should include the open class.

import React, { useState } from "react";

const AccordioMenu = () => {
  const [ selectedItems, setSelectedItems ] = useState([])

  //event-handler accepts an identifer-string as an argument
  const handleSelect = (identifier) => {
     //creates a copy of the original state to avoid state-mutation
     const selectedItemsCopy = [...selectedItems]

     //check if the idenifier that was passed already exists in the state
     if(selectedItemsCopy.includes(identifier)){
         //it already exists, which means the menu-links are expanded
         const foundIndex = selectedItemsCopy.indexOf(identifier)
         //you've clicked it to hide it. so remove the identifier from the state
         selectedItemsCopy.splice(foundIndex, 1)
         setSelectedItems(selectedItemsCopy)
     } else {
        //if identifier was not found in state. then add it.
        setSelectedItems([...selectedItems, identifier])
     }
  }

  return (
    <div className="middle">
      <div className="menu">
        <li className="item" id="profile">
          //set up handler to pass identifier
          <a className='btn' href="#" onClick={() => handleSelect("profile")}>
            Profile
          </a>
          <div className={selectedItems.includes("profile") ? "smenu open" : "smenu"}>
            <a href="">Posts</a>
            <a href="">Pictures</a>
          </div>
        </li>
        <li className="item" id="messages">
          //set up handler to pass identifier
          <a className="btn" href="#" onClick={() => handleSelect("message")}>
            Messages
          </a>
          <div className={selectedItems.includes("messages") ? "smenu open" : "smenu"}>
            <a href="">New</a>
            <a href="">Sent</a>
          </div>
        </li>
        <li className="item" id="logout">
          <a className="btn" href="#">
            Logout
          </a>
        </li>
      </div>
    </div>
  );
};

export default AccordioMenu;

Upvotes: 2

dance2die
dance2die

Reputation: 36895

Christopher Ngo's answer is a good answer that can work.

I just want to provide a different way to handle the same scenario using useReducer.

If you have multiple states that works in conjunction with each other, it sometimes makes it easy to use a reducer, to "co-locate" the related state changes.

import React, { useReducer } from "react";

const initialState = {
  profileClass: 'smenu',
  menuClass: 'smenu',
};

// 👇 You can see which state are related and how they should change together.
// And you can also see from the action type what each state is doing.
const menuClassReducer = (state, action) => {
  switch (action.type) {
    case "mark profile as selected":
      return { profileClass: 'smenu open', menuClass: 'smenu' };
    case "mark menu as selected":
      return { profileClass: 'smenu', menuClass: 'smenu open' };
    default:
      return state;
  }
};

const AccordioMenu = () => {
  const [{profileClass, menuClass}, dispatch] = useReducer(menuClassReducer, initialState);

  const toggleActiveP = () => {
    dispatch({type: 'mark profile as selected'})
  };
  const toggleActiveM = () => {
    dispatch({type: 'mark menu as selected'})
  };

  return (
    <div className="middle">
      <div className="menu">
        <li className="item" id="profile">
          <a className="btn" href="#" onClick={toggleActiveP}>
            Profile
          </a>
                              1️⃣ 👇
          <div className={profileClass}>
            <a href="">Posts</a>
            <a href="">Pictures</a>
          </div>
        </li>
        <li className="item" id="messages">
          <a className="btn" href="#" onClick={toggleActiveM}>
            Messages
          </a>
                           2️⃣ 👇
          <div className={menuClass}>
            <a href="">New</a>
            <a href="">Sent</a>
          </div>
        </li>
        <li className="item" id="logout">
          <a className="btn" href="#">
            Logout
          </a>
        </li>
      </div>
    </div>
  );
};

You can see 1️⃣ & 2️⃣ above that you can simply set the state classes that are returned from the reducer.

Those two classes (menuClass & profileClass) are updated automatically on click as events are dispatched from toggleActiveM & toggleActiveP respectively.

If you plan to do something with the "selected" state, you can simply update the reducer by handling new states and you'd still know how each state are updated together in one place.

import React, { useReducer } from "react";

const initialState = {
  isProfileSelected: false,
  isMenuSelected: false,
  profileClass: "smenu",
  menuClass: "smenu"
};
const menuClassReducer = (state, action) => {
  switch (action.type) {
    case "mark profile as selected":
      return {
        isProfileSelected: true,
        isMenuSelected: false,
        profileClass: "smenu open",
        menuClass: "smenu"
      };
    case "mark menu as selected":
      return {
        isProfileSelected: false,
        isMenuSelected: true,
        profileClass: "smenu",
        menuClass: "smenu open"
      };
    default:
      return state;
  }
};

const AccordioMenu = () => {
  const [
    { profileClass, menuClass, isProfileSelected, isMenuSelected },
    dispatch
  ] = useReducer(menuClassReducer, initialState);

  const toggleActiveP = () => {
    dispatch({ type: "mark profile as selected" });
  };
  const toggleActiveM = () => {
    dispatch({ type: "mark menu as selected" });
  };
  return // do something with newly added states;
};

Upvotes: 2

Related Questions