Search code examples
reactjsaccordioninfinite-loopreactstrap

Toggle Accordion functionality with reactstrap?


Using the reactstrap documentation, I created a pseudo-accordion with collapsible cards. I want to be able to toggle the cards, so that when I click on the card header of list 2 once it will open, and it will close again if I click it consecutively. If I click list 1, list 2 should close and list 1 should open, as expected.

Accordion Component:

import React, { Fragment, useState } from 'react';
import { Collapse, CardBody, Card, CardHeader } from 'reactstrap';

const Lists = () => {
    const [isOpen, setIsOpen] = useState('');
    const [lists, setLists] = useState([
        {
            _id: 'id_abc',
            item_names: ['item 1', 'item 2', 'item 3'],
            list_name: 'List 2',
            author_name: 'Samantha Samson',
        },
        {
            _id: 'id_xyz',
            item_names: ['item 1', 'item 1', 'item 2', 'item 16'],
            list_name: 'List 1',
            author_name: 'John Johnson',
        },
    ]);

return (
    <Fragment>
        {lists.map((list) => (
            <Card key={list._id}>
                <CardHeader onClick={() => setIsOpen(list._id)}>
                    <h4>{list.list_name}</h4>
                </CardHeader>
                <Collapse isOpen={isOpen === list._id ? true : false}>
                    <CardBody>
                        <ul>
                            {list.item_names.map((item) => (
                                <li>{item}</li>
                            ))}
                        </ul>
                    </CardBody>
                </Collapse>
            </Card>
        ))}
    </Fragment>

);

I tried adding a function to the onClick that looked like this:

const handleToggle = (id) => {
    if(isOpen === id) {
        setIsOpen('');
    } else {
        setIsOpen(id);
    }
}

and gave the header:

onClick={handleToggle(list._id)}

but that gave many a too many rerenders/infinite loop error.

I also tried slight variations of that function with the same result.


Solution

  • You have pretty much fixed it with this one:

      const handleToggle = (id) => {
        if (isOpen === id) {
          setIsOpen("");
        } else {
          setIsOpen(id);
        }
      };
    

    So great job! Where you made a mistake is calling it like so:

    onClick={handleToggle(list._id)}
    

    This will call the function on every render and since it updates a state variable, you will get infinite renders.

    It should be:

    onClick={() => handleToggle(list._id)}
    

    Sandbox: https://codesandbox.io/s/hungry-jepsen-1ip0x?file=/src/App.js

    Just FYI: You can simplify your ternary operator as:

    isOpen={isOpen === list._id}