如何根据动态内容正确更新React函数式Accordion组件的MaxHeight
你遇到的这个问题非常典型——当手风琴的内容从父组件传入且动态变化时,DOM渲染完成的时机和我们计算高度的时机不匹配,导致无法获取到正确的scrollHeight。下面给你几个实用的解决方案,按推荐程度排序:
1. 使用ResizeObserver监听内容容器尺寸变化
这是最健壮的方案,ResizeObserver是浏览器原生API,能直接监听元素的尺寸变化,不管内容是因为父组件传值变化、内部状态更新还是异步加载(比如图片加载完成)导致的高度改变,都能准确捕捉到。
修改你的组件代码:
import React, { useEffect, useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronUp, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import './Accordion.css'; const Accordion = ({ title, content, active }) => { const [isActive, setIsActive] = useState(active); const [expanderStyle, setExpanderstyle] = useState({ maxHeight: 0 }); const contentDiv = useRef(); // 同步父组件的active状态 useEffect(() => { setIsActive(active); }, [active]); // 监听内容容器的尺寸变化 useEffect(() => { const updateHeight = () => { if (contentDiv.current && isActive) { setExpanderstyle({ maxHeight: contentDiv.current.scrollHeight + 1 }); } }; // 初始化时先计算一次 updateHeight(); // 创建ResizeObserver实例 const observer = new ResizeObserver(() => { if (isActive) { updateHeight(); } }); if (contentDiv.current) { observer.observe(contentDiv.current); } // 组件卸载时清理监听 return () => { if (contentDiv.current) { observer.unobserve(contentDiv.current); } }; }, [isActive]); const toggle = () => { setIsActive(!isActive); }; return ( <div className="accordion"> <div className="accordion-title"> <div> <button className="btn btn-icon round small" onClick={toggle}> <FontAwesomeIcon icon={faChevronUp} className={`icon rotator ${isActive ? "active" : ""}`} /> </button> </div> <div className="accordion-title--title"> {title} </div> </div> <div ref={contentDiv} className="accordion-content" style={expanderStyle}> {content} </div> </div> ); }; export default Accordion;
这个方案的优势是无需依赖content的引用变化,哪怕内容是同一个React元素但内部DOM更新(比如子组件状态变化),也能正确触发高度计算。
2. 结合requestAnimationFrame和content依赖
如果你的内容变化总是伴随content prop的引用变化(比如父组件每次传新的JSX元素),可以在监听content变化时,用requestAnimationFrame确保DOM已经渲染完成后再计算高度:
useEffect(() => { const updateHeight = () => { if (contentDiv.current) { setExpanderstyle(isActive ? { maxHeight: contentDiv.current.scrollHeight + 1 } : { maxHeight: 0 }); } }; // 用requestAnimationFrame等待DOM更新完成 const rafId = requestAnimationFrame(updateHeight); return () => cancelAnimationFrame(rafId); }, [isActive, content]); // 加入content作为依赖项
注意:如果content是同一个引用(比如父组件的状态变化但没有生成新的JSX),这个方案不会触发,这是它的局限性。
3. 使用useLayoutEffect替代useEffect
useLayoutEffect会在DOM更新完成后同步执行,比useEffect的时机更早,能确保我们获取到最新的DOM尺寸:
useLayoutEffect(() => { if (contentDiv.current) { setExpanderstyle(isActive ? { maxHeight: contentDiv.current.scrollHeight + 1 } : { maxHeight: 0 }); } }, [isActive, content]);
不过要注意,useLayoutEffect会阻塞浏览器绘制,如果计算逻辑复杂可能影响性能,所以优先推荐前两个方案。
额外优化:同步父组件的active状态
你原来的代码中,父组件传入的active prop只会在组件初始化时设置isActive状态,如果父组件后续修改active,子组件的状态不会同步。上面的方案已经加入了同步逻辑,确保父组件能控制手风琴的展开/收起状态。
内容的提问来源于stack exchange,提问作者John Bustos

