在React中使用firstElementChild等直接操作DOM是否是不良实践?
首先明确:React里直接操作DOM并非绝对的不良实践,在动画、精确DOM控制这类场景下,直接操作DOM反而比强行用状态驱动更高效简洁——你的思路完全没问题,而且比那些依赖第三方库或用setTimeout的方案更优雅。
你的实现优点
- 用
useRef存储DOM引用和展开状态,避免了不必要的组件重渲染,性能表现好 - 直接修改DOM样式和ARIA属性,逻辑直观,没有冗余代码
- 加入了按钮临时禁用的逻辑,防止重复点击触发多次动画,细节考虑到位
潜在的待优化点
列表key的问题
你用Math.random()生成列表项的key,这会导致每次组件重渲染时都生成新key,React会销毁并重建所有列表项组件,不仅会丢失组件内部状态,还会严重影响性能,甚至可能导致tabsRef的引用出错。建议替换为item的唯一标识(比如item.id),如果没有唯一标识,用索引i(前提是items不会重新排序)。Ref引用的健壮性
在ContentInner的ref回调中存储element?.parentElement,React的ref回调会在组件挂载、卸载、更新时触发,若父组件props变化导致items重新渲染,可能出现ref引用不准确或null的情况。可以在ref回调中处理卸载场景:ref={(element) => { if (element) { tabsRef.current[i] = { index: i, element: element.parentElement as HTMLElement }; } else { // 组件卸载时移除对应ref tabsRef.current.splice(i, 1); } }}ARIA属性的一致性
初始渲染时AccordionTriggerWrapper的aria-expanded硬编码为"false",如果存在默认展开的项,DOM属性会和实际状态不一致。建议让ARIA属性由状态驱动,比如用useState存储展开索引(而非纯useRef),这样初始状态可以通过props控制,渲染时直接传递:const [expandedIndexes, setExpandedIndexes] = useState<number[]>([]); // 渲染时 <AccordionTriggerWrapper aria-expanded={expandedIndexes.includes(i).toString()} // ...其他属性 >这样虽然会触发组件重渲染,但手风琴组件的项数通常不多,性能影响可以忽略,同时保证了声明式的状态一致性。
DOM结构依赖风险
代码中用firstElementChild、closest("[aria-expanded]")依赖当前DOM结构,若后续修改组件的DOM层级(比如调整Content的子元素、修改触发器的包裹结构),这些方法会直接失效。建议在渲染时直接存储触发器的ref:const triggerRefs = useRef<HTMLElement[]>([]); // 渲染触发器时 <AccordionTriggerWrapper ref={(el) => triggerRefs.current[i] = el} // ...其他属性 >之后在
alterTabs中直接使用triggerRefs.current[tab.index]来设置ARIA属性,无需依赖DOM结构查找。样式逻辑耦合
直接用style.setProperty修改height,把样式逻辑写在了JS中,不符合关注点分离。可以改用CSS类+CSS变量的方式:.Content { height: 0; overflow: hidden; transition: height 0.3s ease; } .Content.is-expanded { height: var(--content-height); }然后在JS中只负责计算高度并切换类:
tab.element.style.setProperty("--content-height", newIndexes.includes(tab.index) ? `${tab.element.firstElementChild.clientHeight}px` : "0px"); tab.element.classList.toggle("is-expanded", newIndexes.includes(tab.index));这样样式逻辑集中在CSS中,JS只处理动态计算部分,代码更易维护。
总结
你的实现本身是可行且高效的,只要解决上述几个潜在问题,代码会更健壮、可维护。相比那些依赖第三方库或用setTimeout的方案,你的思路更简洁直接,完全符合React在特定场景下允许直接操作DOM的原则。
内容的提问来源于stack exchange,提问作者MakingStuffs

