React MUI Accordion筛选后无法折叠展开问题咨询
Accordion组件年份筛选后折叠展开失效问题
问题现象
- 页面内多个Accordion折叠项的渲染数据来自JSON文件
- 页面初始加载时,折叠、展开交互完全正常
- 触发年份筛选后,列表数据可以被正确过滤,但Accordion组件无法响应折叠/展开操作
问题代码
核心逻辑代码
let ifFilter = false; var dataReturn = [], itemlist; export default function EventPastData(data) { data = Object.values(data.data.past); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage] = useState(5); // 调整该数值修改每页展示条数 const [expanded, setExpanded] = useState(false); const [year, setYear] = useState(''); const handleOpen = (panel) => (e, isExpanded) => { setExpanded(isExpanded ? panel : false); }; console.log(data); console.log(dataReturn) function populate(d) { itemlist = d.map((x, index) => { return ( <Accordion key={index} className={`${new Date(x.date).getFullYear()}`} expanded={expanded === `panel${index}`} onChange={handleOpen(`panel${index}`)} > <AccordionSummary expandIcon={<ExpandMoreIcon />}> <Avatar variant="rounded"> <img src={require(`../../img/${x.img}`)} alt={x.img} /> </Avatar> <Paragraph text={x.title} size='1.3rem' /> <Paragraph text={x.date} size='1.1rem' /> </AccordionSummary> <AccordionDetails> <Paragraph text={x.description} size='1.1rem' /> <br /> <Paragraph text={'Venue: ' + x.venue} size='1.1rem' /> <Paragraph text={'Time: ' + x.time} size='1.1rem' /> </AccordionDetails> </Accordion> ) }); }; const handleFilter = (event) => { setYear(event.target.value); dataReturn = []; for (let i = 0; i < data.length; i++) { if (new Date(data[i].date).getFullYear() === event.target.value) { dataReturn.push(data[i]); }; }; if (event.target.value !== '') { ifFilter = true; populate(dataReturn); } else { populate(data); }; }; if (ifFilter !== true) { populate(data); };
页面渲染JSX结构
return ( <div className='test'> <Container sx={{mx: 'auto'}}> <div id='listAccordion'> {currentItems} </div> <Grid container direction="row" justifyContent="center" alignItems="center" spacing={2}> <Grid item sm={1}> <FormControl sx={{ m: 1, minWidth: 80 }} color='secondary'> <InputLabel id="demo-simple-select-autowidth-label">Year</InputLabel> <Select labelId="demo-simple-select-autowidth-label" id="demo-simple-select-autowidth" value={year} onChange={handleFilter} autoWidth label="Year" > <MenuItem value=""> <em>None</em> </MenuItem> <MenuItem value={2019}>2019</MenuItem> <MenuItem value={2020}>2020</MenuItem> <MenuItem value={2021}>2021</MenuItem> <MenuItem value={2022}>2022</MenuItem> </Select> </FormControl> </Grid> <Grid item sm={11}> <Pages itemsPerPage={itemsPerPage} totalItems={itemlist.length} paginate={paginate}></Pages> </Grid> </Grid> <p>You're on page: <strong>{currentPage}</strong></p> </Container> </div> )
根因分析
- 违反React状态驱动规则:将筛选标记、过滤后数据、组件列表存在组件外部的全局可变变量中,这类变量修改不会触发组件重渲染,状态更新和视图渲染不同步,交互事件触发的状态更新无法映射到视图上
- 列表Key值使用错误:用数组下标
index作为Accordion项的key,筛选后数组长度、下标对应的数据项发生变化,React虚拟DOM对比时会错误复用旧的组件实例,导致expanded状态匹配错乱,展开/折叠的控制逻辑和实际组件不对应 - 状态更新时序错误:
setYear是异步更新操作,在同个同步逻辑里直接调用populate生成组件列表时,组件状态还未完成更新,生成的组件实例绑定的状态逻辑是旧版本 - 分页数据未同步更新:筛选后没有重置页码,也没有基于过滤后的数据重新计算当前页展示项
currentItems,导致渲染的列表和实际过滤后的数据不匹配
修复方案
- 移除所有组件外部的全局可变变量,所有和渲染相关的数据全部用
useState或useMemo管理,所有视图更新由状态变化驱动 - 替换列表key:给每个Accordion项使用数据本身的唯一标识(如数据id、标题+日期的组合值)作为key,禁止在动态过滤的列表中使用数组下标作为key
- 派生过滤数据:用
useMemo基于选中的年份直接计算过滤后的列表,不需要手动维护全局的过滤结果变量 - 筛选后自动重置页码到第一页,基于过滤后的列表重新计算分页展示项
- 直接在JSX渲染阶段生成Accordion列表,不需要额外的
populate函数提前给全局变量赋值
修复后核心代码示例
export default function EventPastData(data) { const sourceData = Object.values(data.data.past); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage] = useState(5); const [expanded, setExpanded] = useState(false); const [year, setYear] = useState(''); const handleOpen = (panel) => (e, isExpanded) => { setExpanded(isExpanded ? panel : false); }; // 派生过滤后的数据 const filteredData = useMemo(() => { if (!year) return sourceData; return sourceData.filter(item => new Date(item.date).getFullYear() === year); }, [sourceData, year]); // 筛选变化时重置页码和展开状态 useEffect(() => { setCurrentPage(1); setExpanded(false); }, [year]); // 计算当前页展示数据 const currentItems = useMemo(() => { const start = (currentPage - 1) * itemsPerPage; const end = start + itemsPerPage; return filteredData.slice(start, end).map((x) => { // 用数据唯一值作为panel标识和key,不要用下标 const panelKey = `panel_${x.id || x.title}_${x.date}`; return ( <Accordion key={panelKey} className={`${new Date(x.date).getFullYear()}`} expanded={expanded === panelKey} onChange={handleOpen(panelKey)} > <AccordionSummary expandIcon={<ExpandMoreIcon />}> <Avatar variant="rounded"> <img src={require(`../../img/${x.img}`)} alt={x.img} /> </Avatar> <Paragraph text={x.title} size='1.3rem' /> <Paragraph text={x.date} size='1.1rem' /> </AccordionSummary> <AccordionDetails> <Paragraph text={x.description} size='1.1rem' /> <br /> <Paragraph text={'Venue: ' + x.venue} size='1.1rem' /> <Paragraph text={'Time: ' + x.time} size='1.1rem' /> </AccordionDetails> </Accordion> ) }); }, [filteredData, currentPage, itemsPerPage, expanded, handleOpen]); const handleFilter = (event) => { setYear(event.target.value ? Number(event.target.value) : ''); }; const paginate = (page) => setCurrentPage(page); return ( <div className='test'> <Container sx={{mx: 'auto'}}> <div id='listAccordion'> {currentItems} </div> <Grid container direction="row" justifyContent="center" alignItems="center" spacing={2}> <Grid item sm={1}> <FormControl sx={{ m: 1, minWidth: 80 }} color='secondary'> <InputLabel id="demo-simple-select-autowidth-label">Year</InputLabel> <Select labelId="demo-simple-select-autowidth-label" id="demo-simple-select-autowidth" value={year} onChange={handleFilter} autoWidth label="Year" > <MenuItem value=""> <em>None</em> </MenuItem> <MenuItem value={2019}>2019</MenuItem> <MenuItem value={2020}>2020</MenuItem> <MenuItem value={2021}>2021</MenuItem> <MenuItem value={2022}>2022</MenuItem> </Select> </FormControl> </Grid> <Grid item sm={11}> <Pages itemsPerPage={itemsPerPage} totalItems={filteredData.length} paginate={paginate}></Pages> </Grid> </Grid> <p>You're on page: <strong>{currentPage}</strong></p> </Container> </div> ) }
内容的提问来源于stack exchange,提问作者Ethan Low
相关产品推荐
相关产品推荐

