'React JS - Material UI ListItem (with Collapse API) onClick expands/collapse all sub list items instead of the selected one

I'm implementing Expand/Collapse feature of List using React JS - Material UI ListItem (with Collapse API)

When I click on the ListItem it expands/collapse all sub list items instead of the selected one.

Here is the sample code. Looks like I'm improperly setting the key value in one of the element, but couldn't figure it out. Can someone help me? Please let me know if the information is inadequate

Jaffar

class CategoriesResults extends Component {
  constructor(props) {
    super(props);
    this.state = {
      open: false
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log("Handle Clicked....");
     this.setState(prevState => ({
       open: !prevState.open
     }));
  }

  render() {
    const docs = data.documents;  //this coming from a json file, please see below for the sample json
     return (
      <div>
        <List component='nav' aria-labelledby='nested-list-subheader'>
          {docs.map(doc => {
            return (
              <div key={doc.Id}>
                <ListItem button key={doc.Id} onClick={this.handleClick}>
                  <ListItemText primary={doc.Name} />
                  {this.state.open ? <ExpandLess /> : <ExpandMore />}
                </ListItem>
                <Collapse
                  key={doc.Sheets.Id}
                  in={this.state.open}
                  timeout='auto'
                  unmountOnExit
                >
                  <List component='li' disablePadding key={doc.Id}>
                    {doc.Sheets.map(sheet => {
                      return (
                        <ListItem button key={sheet.Id}>
                          <ListItemIcon>
                            <InsertDriveFileTwoToneIcon />
                          </ListItemIcon>
                          <ListItemText key={sheet.Id} primary={sheet.Title} />
                        </ListItem>
                      );
                    })}
                  </List>
                </Collapse>
                <Divider />
              </div>
            );
          })}
        </List>     
      </div>
    );
  }
}


**Sample JSON**

{
  "documents": [
    {
      "Id": 1,
      "Name": "Category 1",
      "Sheets": [
        {
          "Id": 1,
          "Title": "Title1 "
        },
        {
          "Id": 2,
          "Title": "Title 2"
        },
        {
          "Id": 3,
          "Title": "Title 3"
        }
      ]
    }
}



Solution 1:[1]

All of your ListItems are expanded based on the same state.
If open is true, all of them get in=true in their Collapse component, so all of them are expanded.
To fix that, you should extract your expandable ListItem to a seperate component, which will manage it's own state:

 class CustomizedListItem extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        open: false
      };
      this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
      console.log("Handle Clicked....");
       this.setState(prevState => ({
         open: !prevState.open
       }));
    }

  render(){
  const { doc } = this.props;
  return (
    <div>
      <ListItem button key={doc.Id} onClick={this.handleClick}>
        <ListItemText primary={doc.Name} />
        {this.state.open ? <ExpandLess /> : <ExpandMore />}
      </ListItem>
      <Collapse
        key={doc.Sheets.Id}
        in={this.state.open}
        timeout='auto'
        unmountOnExit
      >
      <List component='li' disablePadding key={doc.Id}>
        {doc.Sheets.map(sheet => {
          return (
            <ListItem button key={sheet.Id}>
              <ListItemIcon>
                {/* <InsertDriveFileTwoToneIcon /> */}
              </ListItemIcon>
              <ListItemText key={sheet.Id} primary={sheet.Title} />
            </ListItem>
          );
        })}
      </List>
    </Collapse>
    <Divider />
    </div>
    )
  }
}

export default class CategoriesResults extends React.Component {
  render() {
    const docs = data.documents;  //this coming from a json file, please see below for the sample json
     return (
      <div>
        <List component='nav' aria-labelledby='nested-list-subheader'>
          {docs.map(doc => {
            return (
              <CustomizedListItem key={doc.id} doc={doc} />
            );
          })}
        </List>     
      </div>
    );
  }
}

I used class component because you used it in your question. If you want to see the same solution with react-hooks, let me know :)

Edit Invisible Backdrop

Solution 2:[2]

I have converted this into a function-based component that uses hooks so people coming from future can understand it easily:

import React, { useState } from 'react'

const CustomizedListItem = ({ doc }) => {
    const [ open, setOpen ] = useState(false)
    const handleClick = () => {
        setOpen(!open)
    }
    
    return (
        <div>
            <ListItem button key={doc.Id} onClick={handleClick}>
                <ListItemText primary={doc.Name} />
                {open ? <ExpandLess /> : <ExpandMore />}
            </ListItem>
            <Collapse
                key={doc.Sheets.Id}
                in={open}
                timeout='auto'
                unmountOnExit
            >
                <List component='li' disablePadding key={doc.Id}>
                    {doc.Sheets.map(sheet => {
                        return (
                            <ListItem button key={sheet.Id}>
                                <ListItemIcon>
                                    {/* <InsertDriveFileTwoToneIcon /> */}
                                </ListItemIcon>
                                <ListItemText key={sheet.Id} primary={sheet.Title} />
                            </ListItem>
                        )
                    })}
                </List>
            </Collapse>
            <Divider />
        </div>
    )
}


export default function CategoriesResults() {
    const docs = data.documents  //this coming from a json file
    return (
        <div>
            <List component='nav' aria-labelledby='nested-list-subheader'>
                {docs.map(doc => {
                    return (
                        <CustomizedListItem key={doc.id} doc={doc} />
                    )
                })}
            </List>
        </div>
    )
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ido
Solution 2