如何阻止Accordion组件内AccordionBody递归重渲染且保留下拉动画
解决全量重渲染问题
所有<AccordionItem/>跟着父组件重渲染的核心原因是React默认父组件状态更新时所有子组件都会触发重渲染,你可以通过以下方式优化:
- 用
React.memo包裹<AccordionItem/>组件,默认会对props做浅比较,只有props变化时才会触发组件重渲染 - 父组件传递给
<AccordionItem/>的回调函数(比如切换激活状态的方法)必须用useCallback包裹,避免每次父组件渲染都生成新的函数引用,导致memo失效 - 所有传递给
<AccordionItem/>的引用类型props(比如自定义样式、配置对象)如果是固定值,要么抽成组件外的常量,要么用useMemo缓存
解决懒渲染与动画兼容问题
你之前直接用{active && <AccordionBody />}导致动画失效的原因是:CSS高度过渡需要目标元素真实存在才能计算尺寸执行动画,切换active的同时直接挂载/卸载内容,会导致过渡触发时内容不存在,自然看不到动画效果,你可以用以下方案兼容两者:
方案1:优先推荐的grid过渡方案
这个方案不需要手动计算内容高度,兼容性也覆盖绝大多数现代浏览器,同时支持懒渲染:
- 给
<AccordionBody/>外层加一个容器元素,用CSS grid的行高过渡实现展开收起动画,避免依赖内容渲染后的高度计算 - 新增
hasOpened状态,仅当条目第一次被激活时才渲染<AccordionBody/>的实际内容,未激活过的条目不会渲染主体,降低首次渲染开销 - 如果需要在条目关闭后卸载主体释放内存,可以监听过渡结束事件,等收起动画执行完成后再卸载内容
示例代码如下:
// AccordionItem 组件(已用memo包裹避免无效重渲染) const AccordionItem = React.memo(({ itemId, isActive, onToggle, header, body }) => { // 仅当第一次激活后才渲染主体内容 const [hasRenderedBody, setHasRenderedBody] = useState(isActive); useEffect(() => { if (isActive) { setHasRenderedBody(true); } }, [isActive]); // 过渡结束后如果是关闭状态,卸载主体 const handleTransitionEnd = (e) => { if (e.propertyName === 'grid-template-rows' && !isActive) { setHasRenderedBody(false); } }; return ( <div className="accordion-item"> <div className="accordion-header" onClick={() => onToggle(itemId)}> {header} </div> <div className={`accordion-body-wrap ${isActive ? 'active' : ''}`} onTransitionEnd={handleTransitionEnd} > {hasRenderedBody && <div className="accordion-body">{body}</div>} </div> </div> ); });
对应CSS样式:
.accordion-body-wrap { display: grid; grid-template-rows: 0fr; overflow: hidden; transition: grid-template-rows 0.3s ease; } .accordion-body-wrap.active { grid-template-rows: 1fr; } .accordion-body { min-height: 0; }
方案2:兼容旧浏览器的手动计算高度方案
如果需要兼容不支持grid过渡的旧浏览器,可以手动计算内容的scrollHeight:
- 激活条目时先挂载主体内容,读取
scrollHeight后设置容器高度为对应值,执行过渡 - 关闭条目时先把容器高度设为0,等过渡结束后再卸载主体内容
内容的提问来源于stack exchange,提问作者Reinier68
相关产品推荐
相关产品推荐

