skip to Main Content

When one is clicked, the others should not expand.

Below is my current code, I have tried to create a separate isOpen state value, but I Ended up messing it even more. Below is the code that you see on the screen-shot.

This first part of the code is the Accordion Item


import { memo, useState } from "react";
import styles from "./AccordionItemFAQ.module.scss";

interface AccordionItemFAQProps {
    title: string;
    content: string;
}

const AccordionItemFAQ: React.FC<AccordionItemFAQProps> = ({ title, content }) => {
    const [isOpen, setIsOpen] = useState(false);

    const handleClick = () => {
        setIsOpen(!isOpen);
    };

    return (
        <div className={styles.containerAll}>
            <button onClick={handleClick} className={styles.containerTitle}>
                {title}
            </button>
            {isOpen && (
                <div className={styles.containerContent}>
                    <div className={styles.splitter}></div>
                    <div className={styles.content}>{content}</div>
                </div>
            )}
        </div>
    );
};

export default memo(AccordionItemFAQ);

Below is the FAQ page code, where i import the Accordion Item to the main page

import styles from "styles/pages/FAQ.module.scss";
import type { LayoutProps } from "@root/layouts";
import { buildMainLayout } from "@root/layouts";
import { useEffect, useState } from "react";
import { FaqTable } from "@root/constants/types";
import axios from "axios";
import AccordionItemFAQ from "@root/components/AccordionItemFAQ";

export default function Faq() {
    const [faqTable, setFaqTable] = useState<FaqTable[]>([]);
    const api = "http://localhost:8088/api/v1/faq";

    const loadFaqTable = () => {
        axios.get(api).then((response) => setFaqTable(response.data));
    };

    useEffect(() => {
        loadFaqTable();
    }, []);

    return (
        <div>
            <h1>FAQ</h1>

            <div className={styles.parentContainer}>
                {faqTable.map((item) => (
                    <AccordionItemFAQ key={item.faqId} title={item.question} content={item.answer}></AccordionItemFAQ>
                ))}
            </div>
        </div>
    );
}

// <-- layout
const layoutProps: LayoutProps = {
    mainClassName: "faq",
    currentPageDescription: "faqPage",
};
Faq.layout = buildMainLayout(layoutProps);


2

Answers


  1. I would control the open/close state and click handler from the parent Faq component. You can pass the id of the selected faq to the click handler to keep track of which faq should be opened. If unselecting (all faqs closed) is needed, you can set the selected state back to null.

      interface AccordionItemFAQProps {
      title: string
      content: string
    }
    
    const AccordionItemFAQ: React.FC<AccordionItemFAQProps> = ({ title, content, handleClick, isOpen }) => {
     
    
      return (
        <div className={styles.containerAll}>
          <button onClick={handleClick} className={styles.containerTitle}>
            {title}
          </button>
          {isOpen && (
            <div className={styles.containerContent}>
              <div className={styles.splitter}></div>
              <div className={styles.content}>{content}</div>
            </div>
          )}
        </div>
      )
    }
    
    export default memo(AccordionItemFAQ)
    
    export default function Faq() {
      const [faqTable, setFaqTable] = useState<FaqTable[]>([])
      const api = 'http://localhost:8088/api/v1/faq'
    
      const [selected, setSelected] = useState<null | number>(null)
    
      const loadFaqTable = () => {
        axios.get(api).then(response => setFaqTable(response.data))
      }
    
      useEffect(() => {
        loadFaqTable()
      }, [])
    
      const openFaq = (i: number) => {
        setSelected(i)
      }
    
      return (
        <div>
          <h1>FAQ</h1>
    
          <div className={styles.parentContainer}>
            {faqTable.map(item => (
              <AccordionItemFAQ
                key={item.faqId}
                title={item.question}
                content={item.answer}
                handleClick={() => openFaq(item.faqId)}
                isOpen={item.faqId === selected}></AccordionItemFAQ>
            ))}
          </div>
        </div>
      )
    }
    
    Login or Signup to reply.
  2. in these cases where you essentially want to determine behaviour based on siblings its probably easiest to delegate the coordination of events, and therefore also state, back up to the parent. So in this case you should handle state and callback determination on the FAQ page component.

    You can add something similar to

    const [activeFaq,setActiveFaq] = useState<Id>();
    

    on the page component, where Id represents whatever datatype you can use as an id that is present on each faq item.

    Then you should extend the props for the individual FAQ item to accept a callback that determines the expected behaviour for when the target is clicked. You will also need to add a prop that represents whether the accordion is open, this is essentially propagating the parent state back down to the individual item.

    This would look something like this

    interface AccordionItemFAQProps {
        title: string;
        content: string;
        isOpen:boolean;
        onClick:()=>void;
    }
    

    Then update the FAQ component to use these props

    const AccordionItemFAQ: React.FC<AccordionItemFAQProps> = ({ title, content, isOpen, onClick }) => {
     
        const handleClick = () => {
            onClick();
        };
    
        return (
         ... //same JSX
        );
    };
    

    Then finally you can pass the props to the new ItemFaq component

    return (
      <div>
        <h1>FAQ</h1>
    
        <div className={styles.parentContainer}>
          {faqTable.map((item) => (
            <AccordionItemFAQ
              key={item.faqId}
              title={item.question}
              content={item.answer}
              //new props passed
              isOpen={item.faqId === activeFaq}
              onClick={() => {
                setActiveFaq((current) =>
                  current === undefined ? item.faqId : current
                );
              }}
            ></AccordionItemFAQ>
          ))}
        </div>
      </div>
    );
    

    However you design the callback depends on different factors so there’s room for improvement or just changing things. You can change it to only call setState if current state undefined, you can also define it in the page component if you dont like inline functions.

    Eventually you probably also want to change the callback to set the state to undefined if called on the same id that is currently active. Maybe like this

    setActiveFaq((current) =>
      current === undefined
        ? item.faqId
        : current === item.faqId
        ? undefined
        : current
    );
    

    Hope it helps as a starting point,
    Best

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search