skip to Main Content

I am new to react so please be kind.

I have an accordion that shows searched terms, I am trying to pass a state down to each child within this accordion but I can’t seem to get the state to toggle when the child is clicked.

So far I have my accordion (I have removed the code around it):

const [isOpen, setIsOpen] = useState(false)

 const toggleAccordion = () => {
     setIsOpen(!isOpen)   
 }

return (
  <div className='grid'>
    {enableSearch && searchComponents}
    {accordionData.length > 0
      ? (
        <section className='accordion' data-kontent-item-id={id}>
          {title &&
            <h2 className='accordion__title'>{title}</h2>}
          {accordionData.map(
            (item, key) =>
              item.title && (
                <AccordionItem
                  key={key}
                  title={item.title}
                  text={item.text}
                  itemId={item.id}
                  onClick={toggleAccordion}
                  onKeyDown={handleKeyDown}
                  isOpen={isOpen}
               />
              )
          )}
        </section>
        )
      : (
          enableSearch && (
            <div
              className='mt-8'
              dangerouslySetInnerHTML={{ __html: noResultsText }}
            />
          )
        )}
  </div>
)

And my accordion item:

const AccordionItem = ({ title, text, itemId, isOpen }) => {

 const contentRef = useRef(null)

  return (
    <div className={`accordion__item ${isOpen ? 'accordion__item-label--expanded' : ''}`} data-kontent-item-id={itemId}>
      <div
        className='accordion__item-label js-accordion__item-label'
        tabIndex={0}
      >
      </div>
      <div
        className='accordion__item-content js-accordion__item-content'
        ref={contentRef}
      >
        <div className='accordion__item-content-wrapper' dangerouslySetInnerHTML={{ __html: text }} />
      </div>
    </div>
  )
}

I would like the isOpen state to change when the accordion item is clicked. I am not getting any errors.

2

Answers


  1. You can create a state in the Accordion that stores an object lookup that maps the accordion data id to a boolean.

    Every time you toggle the item, it will invert the inner state.

    const [openMap, setOpenMap] = useState({});
    
    const toggleOpen = (id) => {
      setOpenMap((currOpenMap) => {
        return {
          ...currOpenMap,
          [id]: !currOpenMap[id],
        };
      });
    };
    

    The following should work:

    import Accordion from "./components/Accordion";
    
    const accordionData = [
      {
        id: "1",
        title: "Accordion Item 1",
        text: "<p>Accordion Item 1 Content</p>",
      },
      {
        id: "2",
        title: "Accordion Item 2",
        text: "<p>Accordion Item 2 Content</p>",
      },
      {
        id: "3",
        title: "Accordion Item 3",
        text: "<p>Accordion Item 3 Content</p>",
      },
    ];
    
    function App() {
      return (
        <div className="App">
          <Accordion
            title="Accordian Title"
            data={accordionData}
            enableSearch
            searchComponents
          />
        </div>
      );
    }
    
    export default App;
    
    /* eslint-disable react/prop-types */
    import { useState } from "react";
    import AccordionItem from "./AccordionItem";
    
    const id = 1;
    const noResultsText = "No results found";
    
    function handleKeyDown(event) {
      if (event.key === "Enter") {
        event.target.click();
      }
    }
    
    export default function Accordion({
      title,
      data,
      enableSearch,
      searchComponents,
    }) {
      const [openMap, setOpenMap] = useState({});
    
      const toggleOpen = (id) => {
        setOpenMap((currOpenMap) => {
          return {
            ...currOpenMap,
            [id]: !currOpenMap[id],
          };
        });
      };
    
      return (
        <div className="grid">
          {enableSearch && searchComponents}
          {data.length > 0 ? (
            <section className="accordion" data-kontent-item-id={id}>
              {title && <h2 className="accordion__title">{title}</h2>}
              {data.map(
                (item, key) =>
                  item.title && (
                    <AccordionItem
                      key={key}
                      title={item.title}
                      text={item.text}
                      itemId={item.id}
                      toggleOpen={toggleOpen}
                      onKeyDown={handleKeyDown}
                      isOpen={openMap[item.id]}
                    />
                  )
              )}
            </section>
          ) : (
            enableSearch && (
              <div
                className="mt-8"
                dangerouslySetInnerHTML={{ __html: noResultsText }}
              />
            )
          )}
        </div>
      );
    }
    
    /* eslint-disable react/prop-types */
    import { useCallback, useRef } from "react";
    
    export default function AccordionItem({ text, itemId, isOpen, toggleOpen }) {
      const contentRef = useRef(null);
    
      const handleClick = useCallback(() => {
        toggleOpen(itemId);
      }, [itemId, toggleOpen]);
    
      return (
        <div
          className={`accordion__item ${
            isOpen ? "accordion__item-label--expanded" : ""
          }`}
          data-kontent-item-id={itemId}
        >
          <div
            className="accordion__item-label js-accordion__item-label"
            tabIndex={0}
          ></div>
    
          <button type="button" onClick={handleClick}>
            Toggle Accordion
          </button>
    
          <div
            className="accordion__item-content js-accordion__item-content"
            ref={contentRef}
          >
            <div
              className="accordion__item-content-wrapper"
              dangerouslySetInnerHTML={{ __html: text }}
            />
          </div>
        </div>
      );
    }
    
    .accordion__item .accordion__item-content {
      display: none;
    }
    
    .accordion__item.accordion__item-label--expanded .accordion__item-content {
      display: block;
    }
    
    Login or Signup to reply.
  2. AccordionItem should pass the onClick prop to an actual DOMNode like a button element so that when it is clicked it can toggle the state.

    Example:

    const AccordionItem = ({
      onClick,
      title,
      text,
      itemId,
      isOpen
    }) => {
      const contentRef = useRef(null);
    
      return (
        <div className={`accordion__item ${isOpen ? 'accordion__item-label--expanded' : ''}`} data-kontent-item-id={itemId}>
          <div
            className='accordion__item-label js-accordion__item-label'
            tabIndex={0}
          >
          </div>
    
          <button type="button" onClick={onClick}>
            Toggle Accordion
          </button>
    
          <div
            className='accordion__item-content js-accordion__item-content'
            ref={contentRef}
          >
            <div className='accordion__item-content-wrapper' dangerouslySetInnerHTML={{ __html: text }} />
          </div>
        </div>
      )
    }
    

    The next issue you’ll hit though is that you are using a single isOpen state that is passed to all accordion components and they will all be toggled together. My suggestion would be to change isOpen from a boolean value to either a nullable string/number type that coincides with the specific accordion you want to have toggled open, or to use an object if you want more than one accordion open at a time.

    Example:

    Single open accordion:

    const [isOpen, setIsOpen] = useState(null);
    
    const toggleAccordion = (id) => {
      setIsOpen(isOpen => isOpen === id ? null : id);
    }
    
    {accordionData
      .filter(item => item.title)
      .map((item) => (
        <AccordionItem
          key={item.id}
          title={item.title}
          text={item.text}
          itemId={item.id}
          onClick={() => toggleAccordion(item.id)}
          onKeyDown={handleKeyDown}
          isOpen={isOpen === item.id}
        />
      )
    )}
    

    Multiple open accordion:

    const [isOpen, setIsOpen] = useState({});
    
    const toggleAccordion = (id) => {
      setIsOpen(isOpen => ({
        ...isOpen,
        [id]: !isOpen[id],
    
      }));
    }
    
    {accordionData
      .filter(item => item.title)
      .map((item) => (
        <AccordionItem
          key={item.id}
          title={item.title}
          text={item.text}
          itemId={item.id}
          onClick={() => toggleAccordion(item.id)}
          onKeyDown={handleKeyDown}
          isOpen={isOpen[item.id]}
        />
      )
    )}
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search