0

Currently my list all have collapses but they are linked to one state for "open" so if I open one list, all the other lists open. What is the best way to keep the collapses separate from each other without having a lot of states for each list.

EDIT: The app is going through an infinite loop

App.tsx

interface IState {
error: any,
intro: any,
threads: any[],
title: any,
}

export default class App extends React.Component<{}, IState> {
    constructor (props : any) {
        super (props);

        this.state = {
            error: "",
            intro: "Welcome to RedQuick",
            threads: [],
            title: ""
        };

        this.getRedditPost = this.getRedditPost.bind(this)
        this.handleClick = this.handleClick.bind(this)
    }

    public getRedditPost = async (e : any) => {
        e.preventDefault();

        const subreddit = e.target.elements.subreddit.value;
        const redditAPI = await fetch('https://www.reddit.com/r/'+ subreddit +'.json');
        const data = await redditAPI.json();

        console.log(data);

        if (data.kind) {
            this.setState({
                error: undefined,
                intro: undefined,
                threads: data.data.children,
                title: data.data.children[0].data.subreddit.toUpperCase()
            });
        } else {
            this.setState({
                error: "Please enter a valid subreddit name",
                intro: undefined,
                threads: [],
                title: undefined
            });
        }
    }

    public handleClick = (index : any)  => {
        this.setState({ [index]: true });
    }

    public render() {
        return (
            <div>
                <Header 
                    getRedditPost={this.getRedditPost}
                />
                <p className="app__intro">{this.state.intro}</p>
                {
                    this.state.error === "" && this.state.title.length > 0 ?
                    <LinearProgress />:
                    <ThreadList 
                        error={this.state.error}
                        handleClick={this.handleClick}
                        threads={this.state.threads}
                        title={this.state.title}
                    />
                }   
            </div>
        );
    }
}

Threadlist.tsx

<div className="threadlist__subreddit_threadlist">
    <List>
        { props.threads.map((thread : any, index : any) => 
            <div key={index} className="threadlist__subreddit_thread">
                <Divider />
                <ListItem button={true} onClick={props.handleClick(index)}/* component="a" href={thread.data.url}*/ >
                    <ListItemText primary={thread.data.title} secondary={<p><b>Author: </b>{thread.data.author}</p>} />
                    {props[index] ? <ExpandLess /> : <ExpandMore />}
                </ListItem>
                <Collapse in={props[index]} timeout="auto" unmountOnExit={true}>
                    <p>POOP</p>
                </Collapse>
                <Divider />
            </div>
        ) }
    </List> 
</div>
dan1st
  • 12,568
  • 8
  • 34
  • 67
Hyokune
  • 217
  • 1
  • 5
  • 12

4 Answers4

11

You have to create a different state for every collapse, i suggest using setState dynamically with the index you got from the map function, you probably have to pass the index param to the handleClick function and change the state based on that

<div className="threadlist__subreddit_threadlist">
    <List>
        { props.threads.map((thread : any, index : any) => 
            <div key={index} className="threadlist__subreddit_thread">
                <Divider />
                <ListItem button={true} onClick={props.handleClick(index)}/* component="a" href={thread.data.url}*/ >
                    <ListItemText primary={thread.data.title} secondary={<p><b>Author: </b>{thread.data.author}</p>} />
                    {props[index] ? <ExpandLess /> : <ExpandMore />}
                </ListItem>
                <Collapse in={props[index]} timeout="auto" unmountOnExit={true}>
                    <p>POOP</p>
                </Collapse>
                <Divider />
            </div>
        ) }
    </List> 
</div>

Your handleClick should look something like this:

public handleClick = (index : any)  => {
        this.setState({ [index]: true });
    }
Community
  • 1
  • 1
Ricardo Costa
  • 704
  • 6
  • 27
  • When changing the handleClick function, it gives me an error "error TS1005: ':' expected." on the openArray[index] part. Also isn't it best to use prevState to allow the collapse to be closed after opening? But I also receive an error when using prevState as well – Hyokune Sep 03 '18 at 10:45
  • 1
    yes you can do that with prevState, i was giving you a small example not the full answer, i edited but it should work as it is. it's giving you a typescript error and with that i can't help you that much – Ricardo Costa Sep 03 '18 at 11:37
  • I also don't know how you created your handleClick because you did not share it, it's hard to help this way – Ricardo Costa Sep 03 '18 at 11:38
  • 1
    https://stackoverflow.com/questions/29280445/reactjs-setstate-with-a-dynamic-key-name – Ricardo Costa Sep 03 '18 at 11:40
  • My handleClick and state is from a different component App.tsx, I've updated the question with my handleClick inside the App.tsx file – Hyokune Sep 03 '18 at 11:55
  • look at the code i did mate, quite different from yours, you want a dynamic state for each individual component not a general one that's why i do [index]: true, try that please – Ricardo Costa Sep 04 '18 at 07:05
  • It doesn't allow me to put the handleClick as this.setState({ [index]: true }); It gives me an argument error saying " Argument of type '{ [x: number]: boolean; }' is not assignable to parameter of type 'IState ..." – Hyokune Sep 04 '18 at 09:48
  • 1
    because you have to pass the index when you can the function in the component like so : props.handleClick(index), did you try my code? – Ricardo Costa Sep 04 '18 at 09:52
  • I've seem to fix that issue by adding [index:number]: any into the IState interface but now I'm getting an error of an infinite loop – Hyokune Sep 04 '18 at 10:28
  • I think that's a whole different question, if you want please accept this answer and create a new one explaining the new problem – Ricardo Costa Sep 04 '18 at 10:31
  • Oh ok, sorry, quite new to stackoverflow. Thanks for the help – Hyokune Sep 04 '18 at 10:38
  • Not a problem man, it's just because we already lost track of the initial problem – Ricardo Costa Sep 04 '18 at 10:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/179375/discussion-between-hyo-and-ricardo-costa). – Hyokune Sep 04 '18 at 11:10
8

I've done this in this way :

function DrawerMenuItems() {
  const [selectedIndex, setSelectedIndex] = React.useState("")

  const handleClick = index => {
    if (selectedIndex === index) {
      setSelectedIndex("")
    } else {
      setSelectedIndex(index)
    }
  }
  return (
    <List
      component="nav"
      aria-labelledby="nested-list-subheader"
      subheader={
        <ListSubheader component="div" id="nested-list-subheader">
          Nested List Items
        </ListSubheader>
      }
    >
      {drawerMenuItemData.map((item, index) => {
        return (
          <List>
            <ListItem
              key={index}
              button
              onClick={() => {
                handleClick(index)
              }}
            >
              <ListItemIcon>
                <item.icon />
              </ListItemIcon>
              <ListItemText primary={item.title} />
              {index === selectedIndex ? <ExpandLess /> : <ExpandMore />}
            </ListItem>
            <Collapse in={index === selectedIndex} timeout="auto" unmountOnExit>
              <List component="div" disablePadding>
                {item.submenu.map((sub, index) => {
                  return (
                    <ListItem button >
                      <ListItemText primary={sub.name} />
                    </ListItem>
                  )
                })}
              </List>
            </Collapse>
          </List>
        )
      })}
    </List>
  )
}
Md Abdul Halim Rafi
  • 1,810
  • 1
  • 17
  • 25
  • I solved mine like this too, but I currently have an issue, how do you make it persist in an open state when you have clicked the nested items? – Favour George Apr 16 '21 at 16:21
  • In that case, you may use an array instead of string in the `useState`. Then just store the item key in it, and remove it on collapse. – Md Abdul Halim Rafi Apr 17 '21 at 06:13
0

you can use ant design collapse component also. I have made a nested collapse component with it . basically, which library use is not matters. What matters is how are you passing the data & how are you controlling the active key. Here is an example of mine...

const ProductCatagoryHierarchy = () => {

 const [tree, setTree] = useState([])

 useEffect( async () => {

    const arr2 = [
            {id: 1, name: 'gender', parent: null, parent_id: null },
            {id: 2, name: 'material', parent: null, parent_id: null},
            { id: 3, name: 'male', parent: 1, parent_name: "gender" },
            { id: 5, name: 'female', parent: 1, parent_name: "gender" },
            { id: 4, name: 'shoe', parent: 3, parent_id: "male" },
        ]

        let newarr=[];
        for(let i=0 ; i< arr2.length; i++ ){

            if(arr2[i].id){
                if(newarr[i] != {} ){
                    newarr[i] = {}
                }
                newarr[i].id = arr2[i].id 
            }
            if( arr2[i].name ){
                newarr[i].name = arr2[i].name 
            }
            if( arr2[i].parent ){
                newarr[i].parent = arr2[i].parent 
            }
            if( arr2[i].parent_id ){
                newarr[i].parent_id = arr2[i].parent_id 
            }

            newarr[i].products = arr2[i].products;
        }

        console.log('newarr', newarr );

        let tree = function (data, root) {
            var t = {};
            data.forEach(o => {
                Object.assign(t[o.id] = t[o.id] || {}, o);
                t[o.parent] = t[o.parent] || {};
                t[o.parent].children = t[o.parent].children || [];
                t[o.parent].children.push(t[o.id]);
            });
            return t[root].children;
        }(newarr, undefined);
        
        console.log('tree ', tree);
        setTree(tree)
 }, [])

  const childFunc = (children) => {
        // console.log('children', children );
        let childPanel = children.map((child, i) => {
            return(
                <Collapse /* defaultActiveKey={child.id} */  
                style={{ margin: '10px 0' }}  key={i} 
                expandIcon={({ isActive }) => 
               <CaretRightOutlined rotate={isActive ? 90 : 0} />}
                >
                    <Panel header={child.name} key={child.id}   >
                    <p>  {child?.products?.length > 0 ? 
                    <p> Products: <strong> 
                  { child.products.map(x =>  x.name  ).join(', ') } 
                  </strong> </p> : 'No Product Available'} </p> 

                    {child?.children?.length > 0 ? 
                    childFunc(child.children) : null }
                    </Panel>
                </Collapse>
            )
        })

        return childPanel; 
    }

    return(
        <div>

        <h1> Category Hierarchy </h1>

        {/* <Button onClick={() => onclick()} > test </Button> */}


        <Collapse  >
            {
                tree.map((x,i,l) => {
                console.log('x,i,l', x, i, l );
                return(
                <Panel header={x.name} key={x.id}  
               style={{ backgroundColor: 'darkgray' }} >
                <p>  {x?.products?.length > 0 ? 
               <p> Products: <strong> 
               { x.products.map(x => x.name).join(', ') } 
               </strong> </p> : 'No Product Available'} </p> 
                {
                    x?.children?.length > 0 ? 
                    childFunc(x.children)
                    :
                    null
                }
                </Panel>
                )
                })
            }

            
        </Collapse>

        </div>
    )
}

export default ProductCatagoryHierarchy;
Sharif Himu
  • 116
  • 2
  • 7
0

This is how I have done it

interface IObjectKeys {
[key: number]: boolean;

}

const [getCollapse, setCollapse] = React.useState<IObjectKeys>({
    0: false,
    1: false,
    2: false,
    3: false,
    4: false,
    5: false,
    6: false
});



const handleCollapse = (target: any)  => {
    setCollapse({ [target]: !getCollapse[target] });
}

And you can use like this

<Collapse in={getCollapse[1]} timeout="auto" unmountOnExit>
                            <ListItem>
                                <Typography>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Convallis convallis tellus id interdum velit</Typography>
                            </ListItem>
                        </Collapse>

<Collapse in={getCollapse[2]} timeout="auto" unmountOnExit>
                            <ListItem>
                                <Typography>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Convallis convallis tellus id interdum velit</Typography>
                            </ListItem>
                        </Collapse>
alphacat
  • 61
  • 1
  • 4