0

Currently, all accordion panels are being toggled simultaneously. I've tried passing in the index to the click handler, but no luck. How do I compare the current index with the current setActive variable to open and close accordion panels individually? The data I'm working with in production do not have unique ids which is why I'm using index. Thanks for any suggestions!

demo: https://codesandbox.io/s/react-accordion-using-react-hooks-forked-fup4w?file=/components/Accordion.js:141-1323

const Accordion = (props) => {
  const [setActive, setActiveState] = useState(0);
  const [setHeight, setHeightState] = useState("0px");
  const [setRotate, setRotateState] = useState("accordion__icon");

  const content = useRef(null);

  const toggleAccordion = (index) => {
    setActiveState(setActive === index ? "active" : "");
    setHeightState(
      setActive === "active" ? "0px" : `${content.current.scrollHeight}px`
    );
    setRotateState(
      setActive === "active" ? "accordion__icon" : "accordion__icon rotate"
    );
  }

  return (
    <div>
      {data.map((item, index) => (
        <div key={index} className="accordion__section">
          <button
            className={`accordion ${setActive}`}
            onClick={() => toggleAccordion(index)}
          >
            <p className="accordion__title">{item.title}</p>
            <Chevron className={`${setRotate}`} width={10} fill={"#777"} />
          </button>
        <div
          ref={content}
          style={{ maxHeight: `${setHeight}` }}
          className="accordion__content"
        >
          <div>{item.content}</div>
       </div>
    </div>
    ))}
  </div>
  );
};

1 Answers1

2

The problem in your code is that you are generating a general setActive state that is then passed to all your item in your map function. You have to change your state management in order to be able to find for each of the item if they are active or not. I'd rework a bit your component:

const Accordion = (props) => {

  const [activeIndex, setActiveIndex] = useState(0);

  const content = useRef(null);
    
  return (
    <div>
      {data.map((item, index) => {
         const isActive = index === activeIndex
        return (
        <div key={index} className="accordion__section">
          <button
            className={`accordion ${isActive ? "active" : ""}`}
            onClick={() => setActiveIndex(index)}>
            <p className="accordion__title">{item.title}</p>
            <Chevron className={`${isActive ? "accordion__icon" : "accordion__icon rotate" }`} width={10} fill={"#777"} />
          </button>
        <div
          ref={content}
          style={{ maxHeight: `${isActive ? "0px" : `${content.current.scrollHeight}px`}` }}
          className="accordion__content">
          <div>{item.content}</div>
       </div>
    </div>
    )})}
  </div>
  );
};

The idea is that for each item in loop you find which on is active and then assign the classes / styles according to this. Could be even more refactored but I let you clean up now that the idea should be there :)

Ivo
  • 2,308
  • 1
  • 13
  • 28
  • Appreciate the help! This is definitely the right idea. I'm getting an an error TypeError: Cannot read property 'scrollHeight' of null – Michael Morgan Oct 28 '21 at 06:51
  • The problem is that you are assigning a ref, supposed to be unique, in a map (a loop) on your elements, so each elements is assigning itself to the ref and that can not work :) . I invite you to check this issue that should probably solve it and which is not directly related to your first problem: https://stackoverflow.com/questions/52448143/how-to-deal-with-a-ref-within-a-loop – Ivo Oct 28 '21 at 07:03