You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React+Storybook中实现数组生成的嵌套下拉菜单,解决同状态全部展开问题

问题描述

我需要使用React和Storybook实现一款下拉菜单,菜单内部的多级子下拉菜单需从数组动态生成,每个子下拉可包含多个元素。目前遇到的问题是:通过map方法遍历生成下拉项时,所有下拉菜单会同时开关,原因是它们共用同一个state,我不清楚如何为每个下拉菜单配置独立状态。

现有实现代码

export const Staff = ({ title, titleS, titleT, titleF, pos, name, text, post, date, onClick,  ...props }) => {
    const [active, setActive] = useState(false);
    const [activeS, setActiveS] = useState(false);

    const show = () => {
        setActiveS(!activeS)
    }

    return (
       <>
           <div className={['dropdown', active ? 'dropdown--active' : ''].join(' ')}>
               <div className="dropdown__header" onClick={()=> {setActive(!active)} } >
                   <h5>{title}</h5>
               </div>
               <div className="dropdown__body">
                           <>
                                       <>
                                          <div className='task' onClick={show}>
                                              <Icon icon="bi:arrow-right-short" className={['dropdownBodyActive__prev', activeS ? 'dropdownBodyActive__icon': 'dropdownBodyActive__prev'].join(' ')} />
                                              {pos.map((item) => {
                                                  return (
                                                    <>
                                                        <div className="task__content">
                                                            <h4 className="task__name" >{item.name}</h4>
                                                            <h4 className="task__meeting">
                                                            </h4>
                                                        </div>
                                                        <div className='task__date'>
                                                            {item.text}
                                                            <i className='uil uil-ellipsis-h'></i>
                                                        </div></>
                                                                                                      )
                                              })}

                                          </div>

                                           {post.map((item) => {
                                               return (
                                                   <div className={['position', activeS ? 'dropdownBodyActive': 'dropdownBody'].join(' ')}>
                                                       <div className="position__content">
                                                           <ReactSVG src={VecIcon} className='position__icon'/>
                                                           <h4 className="position__name" >{item.name}</h4>
                                                           <h4 className="position__meeting">
                                                               {/*<span>New</span>*/}
                                                           </h4>
                                                           <i className='uil uil-ellipsis-h position__dot'></i>
                                                           <div className='position__date'>
                                                               {item.text}
                                                           </div>
                                                       </div>

                                                   </div>
                                               )
                                           })}

                                       </>


                           </>

               </div>
           </div>

       </>

    );
};



Staff.propTypes = {
    title: PropTypes.string,
    onClick: PropTypes.func,
    text:PropTypes.string,
    date: PropTypes.object,
    pos: PropTypes.arrayOf(
        PropTypes.shape({
            text: PropTypes.string,
            name: PropTypes.string,
        })
    ),
    post: PropTypes.arrayOf(
        PropTypes.shape({
            name: PropTypes.string
        })
    ),
};

Staff.defaultProps = {
    title: '',
    pos: [],
    post: []

};

对应stories代码

const Template = (args) => <div style={{maxWidth: "1220px"}}>
    <Staff {...args}
            text='Jankov'
    >

    <Item

    />




</Staff>


    <StaffS {...args}

            >

        <Item    />
        <Item    />

    </StaffS>

    <StaffT {...args} >

        <Item    />
        <Item    />

    </StaffT>

    <StaffF {...args} >

        <Item  />
        <Item   />

    </StaffF>


</div>;

export const Normal = Template.bind({});
Normal.args = {
    title: 'HEADQUARTERS',
    titleS: 'Design',
    titleT: 'Marketing',
    titleF: 'PR/Backoffice',
    date: {
    text: "2 Branch Office",


},


    pos: [
        {
            name: 'Development',
            text: ''
        },
        {
            name: 'České Budějovice',
            text: '2 Branch Office',

        },
    ],

    post: [
        {
            name: 'Jankov',
            text: '',


        },
        {
            name: 'České Budějovice',
            text: '2 Branch Office',

        },
        {
            name: 'Jankov',
            text: '',

        }
    ]
};

效果对比

  • 当前实现效果:
    当前实现效果
  • 期望实现效果:
    期望实现效果

解决方案

核心思路是把每个需要独立开关的子下拉项抽成单独的组件,让每个组件内部维护自己的开关状态,就不会出现所有菜单联动的问题。

调整步骤

  1. 抽离独立的子下拉组件,内部维护自己的activeS状态:
// 抽离的子下拉组件,每个实例独立维护状态
const SubDropdown = ({ posItem, postList }) => {
  const [activeS, setActiveS] = useState(false);
  const show = () => setActiveS(!activeS);

  return (
    <>
      <div className='task' onClick={show}>
        <Icon icon="bi:arrow-right-short" className={['dropdownBodyActive__prev', activeS ? 'dropdownBodyActive__icon': 'dropdownBodyActive__prev'].join(' ')} />
        <div className="task__content">
          <h4 className="task__name" >{posItem.name}</h4>
          <h4 className="task__meeting"></h4>
        </div>
        <div className='task__date'>
          {posItem.text}
          <i className='uil uil-ellipsis-h'></i>
        </div>
      </div>
      {postList.map((item, index) => (
        <div key={index} className={['position', activeS ? 'dropdownBodyActive': 'dropdownBody'].join(' ')}>
          <div className="position__content">
            <ReactSVG src={VecIcon} className='position__icon'/>
            <h4 className="position__name" >{item.name}</h4>
            <h4 className="position__meeting">
              {/*<span>New</span>*/}
            </h4>
            <i className='uil uil-ellipsis-h position__dot'></i>
            <div className='position__date'>
              {item.text}
            </div>
          </div>
        </div>
      ))}
    </>
  )
}
  1. 修改原来的Staff组件,直接遍历pos数组渲染抽离的SubDropdown组件即可:
export const Staff = ({ title, pos, post, onClick, ...props }) => {
  const [active, setActive] = useState(false);

  return (
    <div className={['dropdown', active ? 'dropdown--active' : ''].join(' ')}>
      <div className="dropdown__header" onClick={()=> {setActive(!active)} } >
        <h5>{title}</h5>
      </div>
      <div className="dropdown__body">
        {/* 遍历渲染每个独立的子下拉,状态互相隔离 */}
        {pos.map((posItem, index) => (
          <SubDropdown key={index} posItem={posItem} postList={post} />
        ))}
      </div>
    </div>
  );
};
  1. 如果你的post数组是每个pos项对应独立的,就给pos的每个元素加post字段,渲染的时候传对应的数据即可,不需要改其他逻辑。

其他可选方案

如果不想抽组件,也可以把状态改成存储当前激活的子菜单唯一标识(比如pos项的id或者name),点击的时候判断当前点击的标识和存储的是否一致即可,适合数据量不大的场景:

// 把activeS改成存储激活项的唯一标识
const [activeKey, setActiveKey] = useState(null);
// 点击的时候传当前项的唯一值
const show = (key) => {
  setActiveKey(activeKey === key ? null : key);
}
// 渲染的时候判断
className={['position', activeKey === item.name ? 'dropdownBodyActive': 'dropdownBody'].join(' ')}

内容的提问来源于stack exchange,提问作者PrinceLoren

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 04:21:03