javascriptreactjsjsxreactstrap

Using Reactstrap: How to toggle only one Collapse at a time?


I am using Reactstrap to open and collapse multiple cards. Once opened, they stay open, and since I plan to use more of them (for articles), this will be a mess. Once I click on a button and open a card, I would like the others to close, so only one card is displayed at a time. How can I achieve this?

    const [isOpenInfo, setIsOpenInfo] = useState(false);
    const toggleInfo = () => setIsOpenInfo(!isOpenInfo);

    const [isOpenArticle1, setIsOpenArticle1] = useState(false);
    const toggleArticle1 = () => setIsOpenArticle1(!isOpenArticle1);

    const [isOpenArticle2, setIsOpenArticle2] = useState(false);
    const toggleArticle2 = () => setIsOpenArticle2(!isOpenArticle2);

In my menu, I have a button "More Info", when clicked, it opens a list of collapsed articles and when clicking on each title, it opens the article (but I just want one article to open at a time). So it's like a collapse inside a collapse...

<Button className="info-button" color="primary" onClick={toggleInfo}>
  More Info
</Button>

<Collapse isOpen={isOpenInfo}>
    <Card className="card">
        <CardBody className="card-body">

            <div className="section section-articles">
                <div className="articles-buttons">
                    <Button
                        className="article2-button"
                        color="primary"
                        onClick={toggleArticle2}
                    >
                      <h3>Article 2</h3>
                    </Button>
                    <Button
                        className="article1-button"
                        color="primary"
                        onClick={toggleArticle1}
                    >
                    <h3>Article 1</h3>
                    </Button>
               </div>

<Collapse isOpen={isOpenArticle2}>
    <Card className="card">
        <CardBody className="card-body">
            <Article2 />
        </CardBody>
    </Card>
</Collapse>
<Collapse isOpen={isOpenArticle1}>
    <Card className="card">
        <CardBody className="card-body">
            <Article1 />
        </CardBody>
    </Card>
</Collapse>


           </div>
        </CardBody>
    </Card>
</Collapse>

Solution

  • You can use an object as state with a callback function when toggling the More Info collapse and then utilize a simple string to determine which article should be opened when the main Collapse is open and a Button was clicked inside of it.

    For example, updating whether or not the main collapse is open:

    const toggleMoreInfo = () => {
      setState(prevState => {
        // this gives us access to the current state when setState is executed
        // then we can inverse a boolean when the More Info button is clicked
        return {
         article: "", // resets the open article
         moreInfoOpen: !prevState.moreInfoOpen // false => true || true => false
        }
      })
    }
    

    For example, updating which article should be opened:

     const handleArticleOpen = (article) => {
        setState((prevState) => 
          return {
          // keep whatever is in state as is by spreading it out (in this case, "moreInfoOpen" stays unchanged)
          ...prevState, // 
          // and just override the article with a passed in string
          article
        }));
      };
    

    When dealing with coupled state, I like to use objects over individual states, since it's easier to keep both sets of state in sync. For a demo and the full code, look below...


    Working demo:

    Edit ReactStrap Collapse Example


    Code

    import * as React from "react";
    import { Button, Card, CardBody, Collapse } from "reactstrap";
    import "./styles.css";
    import "bootstrap/dist/css/bootstrap.min.css";
    
    export default function App() {
      const [state, setState] = React.useState({
        articleOpen: "",
        moreInfoOpen: false
      });
      const { article, moreInfoOpen } = state;
    
      const toggleMoreInfo = () => {
        setState((prevState) => ({
          article: "",
          moreInfoOpen: !prevState.moreInfoOpen
        }));
      };
    
      const handleArticleOpen = (article) => {
        setState((prevState) => ({
          ...prevState,
          article
        }));
      };
    
      return (
        <div className="app">
          <Button className="info-button" color="primary" onClick={toggleMoreInfo}>
            More Info
          </Button>
          <Collapse isOpen={moreInfoOpen}>
            <Card className="card">
              <CardBody className="card-body">
                <div className="section section-articles">
                  <div className="articles-buttons">
                    <Button
                      className="article2-button"
                      color="primary"
                      onClick={() => handleArticleOpen("2")}
                    >
                      <h3>Article 2</h3>
                    </Button>
                    <Button
                      className="article1-button"
                      color="primary"
                      onClick={() => handleArticleOpen("1")}
                    >
                      <h3>Article 1</h3>
                    </Button>
                  </div>
    
                  <Collapse isOpen={article === "2"}>
                    <Card className="card">
                      <CardBody className="card-body">
                        <div>Article 2</div>
                      </CardBody>
                    </Card>
                  </Collapse>
                  <Collapse isOpen={article === "1"}>
                    <Card className="card">
                      <CardBody className="card-body">
                        <div>Article 1</div>
                      </CardBody>
                    </Card>
                  </Collapse>
                </div>
              </CardBody>
            </Card>
          </Collapse>
        </div>
      );
    }