Search code examples
jqueryreactjsaccordion

How to translate `$(this).toggleClass()` from jQuery to React


I just started learning React and I'm building a website in Next.js and I'm stuck on the basics.
I'm a web designer and CSS developer, I've been using jQuery for 10 years mainly for toggling classes on elements on clicks, hover, etc. Now I want to do the same in React, but it's not as straightforward as jQuery and I just can't wrap my head around it 'cause it's a bit too logical for me.
Here is an example: toggle open only one accordion panel at a time:

$(".panel").on("click", function() {
  $(".panel").not($(this)).removeClass("open")
  $(this).toggleClass("open")
})
.accordion {
  max-width: 300px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  font-family: Arial, sans-serif;
}

.panel {
  width: 100%;
  border: 1px solid black;
  cursor: pointer;
  margin-bottom: 10px;
}

.panel h6 {
  font-size: 14px;
  margin: 0;
  padding: 10px;
  background: wheat;
}

.panel p {
  font-size: 0;
  padding: 0 10px;
  margin: 0;
  transition: all 0.3s linear;
}

.panel.open p {
  font-size: 14px;
  padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<div class="accordion">
  <div class="panel">
    <h6>Panel One</h6>
    <p>Lorem ipsum dolor sit amet</p>
  </div>
  <div class="panel">
    <h6>Panel Two</h6>
    <p>Lorem ipsum dolor sit amet</p>
  </div>
  <div class="panel">
    <h6>Panel Three</h6>
    <p>Lorem ipsum dolor sit amet</p>
  </div>
</div>

Just 3 lines of JS, job done. Now I want to do the same in React, but have no idea how. My attempt:

import { useState } from "react"

export default function Accordion() {
    const [isActive, setIsActive] = useState(false)
    let classNames = 'panel ' + (isActive ? 'active' : '')

    function handleClick() {
        setIsActive(!isActive)
    }
    
    return (
        <div className="accordion">
            <Panel className={classNames} onClick={handleClick}>
                <h6>Panel One</h6>
                <p>Lorem Ipsum Dolor Sit Amet</p>
            </Panel>
            <Panel className={classNames} onClick={handleClick}>
                <h6>Panel Two</h6>
                <p>Lorem Ipsum Dolor Sit Amet</p>
            </Panel>
            <Panel className={classNames} onClick={handleClick}>
                <h6>Panel Three</h6>
                <p>Lorem Ipsum Dolor Sit Amet</p>
            </Panel>
        </div>
    )
}

function Panel({ children, className, onClick }) {
    return (
        <div className={className} onClick={onClick}>
            {children}
        </div>
    )
}

But it doesn't work well, when I click on a panel, all panels open/close together.
All tutorials online use .map(), but ideally I'd like to avoid it.
I understand that in React useState triggers a re-render, but useRef() doesn't, so I thought I can combine them, but then again, it's not that I can do panel.current.toggleClass('open'), so I don't know.

TLDR: Can anyone help me understand the basics of React, specifically how to "translate" the jQuery function on click $(element).not($(this)).removeClass(class); $(this).toggleClass(class)?


Solution

  • Since here your requirement is to control each Panel separately, and then only one of them can be open at time (according to your example), then instead of a boolean your state must store what panel is open. And since you don't want to use map, and to avoid even more repetition, you should consider moving the classNames into the Panel component.

    export default function Accordion() {
      const [activePanel, setActivePanel] = useState(null);
    
      const handleClick = (panelId) => {
        setActivePanel((prevActivePanel) => (
          prevActivePanel === panelId ? null : panelId
        ));
      };
     
      return (
        <div className="accordion">
          <Panel
            isActive={activePanel === 1}
            onClick={() => handleCLick(1)}
          >
            <h6>Panel One</h6>
            <p>Lorem Ipsum Dolor Sit Amet</p>
          </Panel>
    
          <Panel
            isActive={activePanel === 2}
            onClick={() => handleClick(2)}
          >
            <h6>Panel Two</h6>
            <p>Lorem Ipsum Dolor Sit Amet</p>
          </Panel>
    
           <Panel
             isActive={activePanel === 3}
             onClick={() => handleClick(3)}
           >
             <h6>Panel Three</h6>
             <p>Lorem Ipsum Dolor Sit Amet</p>
           </Panel>
        </div>
      );
    }
    
    function Panel({ children, isActive, onClick }) {
      const classNames = 'panel ' + (isActive ? 'active' : '');
    
      return (
        <div className={className} onClick={onClick}>
          {children}
        </div>
      );
    }