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

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>
    )

根因分析

  1. 违反React状态驱动规则:将筛选标记、过滤后数据、组件列表存在组件外部的全局可变变量中,这类变量修改不会触发组件重渲染,状态更新和视图渲染不同步,交互事件触发的状态更新无法映射到视图上
  2. 列表Key值使用错误:用数组下标index作为Accordion项的key,筛选后数组长度、下标对应的数据项发生变化,React虚拟DOM对比时会错误复用旧的组件实例,导致expanded状态匹配错乱,展开/折叠的控制逻辑和实际组件不对应
  3. 状态更新时序错误:setYear是异步更新操作,在同个同步逻辑里直接调用populate生成组件列表时,组件状态还未完成更新,生成的组件实例绑定的状态逻辑是旧版本
  4. 分页数据未同步更新:筛选后没有重置页码,也没有基于过滤后的数据重新计算当前页展示项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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 11:18:26