如何在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: '', } ] };
效果对比
- 当前实现效果:

- 期望实现效果:

解决方案
核心思路是把每个需要独立开关的子下拉项抽成单独的组件,让每个组件内部维护自己的开关状态,就不会出现所有菜单联动的问题。
调整步骤
- 抽离独立的子下拉组件,内部维护自己的
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> ))} </> ) }
- 修改原来的
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> ); };
- 如果你的
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
相关产品推荐
相关产品推荐

